【LeetCode】周赛#204 复盘
这是我的第一次周赛复盘,若叙述中有疏漏之处,欢迎各位大佬批评指点~
周赛战绩
本次周赛在时间范围内只做出一道,第二道题额外用了15min才通过,上次周赛可能运气太好,遇到的都是自己比较熟悉的题目,但这次明显有点吃力,因此在赛后我重新把题目做了一遍,对于已经AC的题目,重做一遍理清思路;对于当时没做出的题目,则是参考了大佬们的解题方法,以此积累经验。
赛题复盘
5499. 重复至少 K 次且长度为 M 的模式
【问题描述】
给你一个正整数数组 arr
,请你找出一个长度为 m
且在数组中至少重复 k
次的模式。
模式 是由一个或多个值组成的子数组(连续的子序列),连续 重复多次但 不重叠 。 模式由其长度和重复次数定义。
如果数组中存在至少重复k
次且长度为 m
的模式,则返回 true
否则返回 false
。
提示:
2 <= arr.length <= 100
1 <= arr[i] <= 100
1 <= m <= 100
2 <= k <= 100
【注意点&思路】
这道题的思路就是模拟,主要是要读懂题意,题目中要求的找到至少连续重复k次的模式。当时做题时我就忽视了连续的条件,耗了不少时间;此外,最好举具体的实例,在纸上模拟一变,这样有助于考虑得更全面,不至于犯“数组越界”等错误。
【代码】
几个容易漏的语句在后面用//mark
作了标记
class Solution {
public:
bool containsPattern(vector<int>& arr, int m, int k) {
vector<int> vt;
for (int i=0; i<arr.size()-m; i++){
vt.clear();
for (int j=i; j<i+m; ++j)
vt.push_back(arr[j]);
int cnt = 1;
for (int j=i+m; j<arr.size(); j+=m){
if(j+m > arr.size()) //mark 防止越界
continue;
int flag = 1;
for (int r=j; r<j+m; ++r){
if(arr[r] != vt[r-j]){
flag = 0;
break;
}
}
if(flag == 0) //mark 连续条件
break;
cnt++;
if(cnt >= k)
return true;
}
}
return false;
}
};
【复杂度分析】
时间复杂度: O ( n 2 ) O(n^{2}) O(n2),原本应为 O ( n 2 − m 2 ) O(n^{2} - m^{2}) O(n2−m2),但由于模式长度越接近n,循环次数会越少,甚至为0,因此m<<n,故复杂度为 O ( n 2 ) O(n^{2}) O(n2)
空间复杂度: O ( m ) O(m) O(m),用在了存储模式的容器上
5500. 乘积为正数的最长子数组长度
这道题我也在leetcode主页上发布了题解,那里会更加简洁和清晰,点此进入该题解页面
下面说明下我当时的思路以及一些改进:
【问题描述】
给你一个整数数组 nums
,请你求出乘积为正数的最长子数组的长度。
一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。
请你返回乘积为正数的最长子数组长度。
提示:
1 <= nums.length <= 10^5
-10^9 <= nums[i] <= 10^9
【注意点&思路】
按0元素分割数组
首先看数据范围,结合int
的最大取值大约为 2 × 1 0 9 2 \times 10^{9} 2×109,故此题肯定不能通过单纯数据元素的运算来解决;再看数组元素范围,最大值为10万,平方一下就是 1 0 10 10^{10} 1010,故不要想着能用时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)的暴力算法解决问题。
一般压缩时间复杂度我们通常会想到动态规划和记忆化搜索,当时做题时也确实想了,但就是没有抽象出对问题的合适描述。但至少获得了以下思路:
- 最长子数组不能含有0元素,因此可以借0来把
nums
分隔成数个子数组 - 最长子数组中要么没负数,要么就有成对的负数
根据以上对问题的嗅探,我用一个容器zero
存储0元素的下标(当然如果容器大小为0,就代表没有0元素),通过0元素将整个数组分隔成zero.size()+1
个不含0子数组,接着分别对这些子数组求最长子数组,然后找到这些最长子数组中的最大值返回即可。
因此现在的核心问题是,如何找寻不含0元素的子数组中的最长子数组。我们讨论以下几种情况:
- 该数组中的所有元素均为正
- 该数组中有偶数个负数
- 该数组中有奇数个负数
很明显情况1和2是等价的,最长子数组就是自己,下面通过举一个实例详细讨论情况3:
假设有这样一个数组 {-1 3 5 -7 9 -11 13 15}
,很容易得到它的最长子数组为 {3 5 -7 9 -11 13 15}
,长度为7
。
程序肯定是循环处理的,我们从第一个元素(下标为0)开始一步步模拟找寻最长子数组的过程,目的就是要想办法合理地使这个过程得到我们预期的答案。
不妨设数组为x
,最大长度为maxLen = 0
,当前最大长度为curMax = 0
,负数个数为ctf = 0
初次模拟
Loop 1 : x[0] < 0 → \rightarrow → ctf++ (ctf == 1) ,maxLen = 0,curMax = 0
Loop 2 : x[1] > 0 → \rightarrow → curMax++ (curMax == 1)
Loop 3 : x[2] > 0 → \rightarrow → curMax++ (curMax == 2)
Loop 4 : x[3] < 0 → \rightarrow → ctf++ (ctf == 2) → \rightarrow → 负负得正,因此 curMax = curMax + ctf (curMax == 4) → \rightarrow → ctf = 0, maxLen = max(maxLen, curMax) (maxLen == 4)
Loop 5 : x[4] > 0 → \rightarrow → curMax++ (curMax == 5) , maxLen = max(maxLen, curMax) , (maxLen == 5)
Loop 6 : x[5] < 0 → \rightarrow → ctf++ (ctf == 1) ,maxLen不变,curMax = 0
Loop 7 : x[6] > 0 → \rightarrow → curMax++ (curMax == 1)
Loop 8 : x[7] > 0 → \rightarrow → curMax++ (curMax == 2)
最终:
maxLen = max(maxLen, curMax) → \rightarrow → max(5, 2) → \rightarrow → 5 ≠ \ne = 7
这显然是不符合预期的,以上算法得到的最长子数组为{-1 3 5 -7 9}
,我们把第一个负数-1
考虑了进去,倘若给定的数组是{13 15 -1 3 5 -7 9 -11}
,那么按照上述步骤的确没错。其中的根本问题是第一个负数前的元素个数和最后一个负数后的元素个数比较,哪个更多就把这个负数加入到最长子数组中
因此,我们可以用变量negStartPos
记录第一个负数位置,它的隐藏含义是第一个负数前正数的个数,而变量curMax
退出循环时存储的是最后一个负数后正数的个数,然后在循环后再加如下判断:
if(ctf == 1){
if(negStartPos < curMax)
maxLen = maxLen - negStartPos + curMax;
}
这样在上述Loop 8后得到的结果便是maxLen = 5 - 0 + 2 → \rightarrow → 7了。
整理上述思路,全面考虑上述三种情况,会发现无需用到maxLen
和curMax
,只要再添一个 negEndPos
记录最后一个负数即可。从而能写出求不含0数组中的最长子数组的函数,这里我们声明的函数原型为int getMaxLen(const vector<int>& arr, int l, int r);
代表arr
在[l, r]区间内的元素所构成的子数组中乘积为正的最大长度,并默认这个子数组不含0元素。
int getMaxLen(const vector<int>& arr, int l, int r){
if(l>r)
return 0;
int negStartPos=-1, negEndPos = -1, ctf = 0;
for (int i=l; i<=r; ++i){
if(arr[i] > 0){
continue;
}
else{
ctf++;
if(negStartPos == -1) //记录第一个负数所在位置
negStartPos = i;
negEndPos = i; //记录最后一个负数所在位置
}
}
if(ctf%2 == 0) //全为正数或偶数个负数
return r-l+1;
else{
int more = max(negStartPos-l, r-l-(negEndPos-l)); //注意位置是相对的
return more + negEndPos - negStartPos;
}
}
核心函数完成后,我们在主函数中先遍历一遍数组,存储所有0元素的位置,然后按0元素分隔成多个子数组,对每个子数组求最大长度,并找其中最长的即可。
【代码】
完整代码如下:
class Solution {
public:
int getMaxLen(const vector<int>& arr, int l, int r){
if(l>r)
return 0;
int negStartPos=-1, negEndPos = -1, ctf = 0;
for (int i=l; i<=r; ++i){
if(arr[i] > 0){
continue;
}
else{
ctf++;
if(negStartPos == -1)
negStartPos = i;
negEndPos = i;
}
}
if(ctf%2 == 0) //全为正数或偶数个负数
return r-l+1;
else{
int more = max(negStartPos-l, r-l-(negEndPos-l));
return more + negEndPos - negStartPos;
}
}
int getMaxLen(vector<int>& nums) {
int n = nums.size();
vector<int> zero; //存储0元素的下标
for (int i=0; i<n; ++i)
if(nums[i] == 0)
zero.push_back(i);
if(zero.size() == 0) //没有0元素,则直接得最大长度
return getMaxLen(nums, 0, n-1);
int maxLen = 0;
int start=0;
for (const auto& zeroPos: zero){
if(maxLen >= zeroPos - start){
//优化
start = zeroPos+1;
continue;
}
maxLen = max(maxLen, getMaxLen(nums, start, zeroPos-1));
start = zeroPos+1;
}
//别忘了最后一个0之后的子数组
maxLen = max(maxLen, getMaxLen(nums, start, n-1));
return maxLen;
}
};
【复杂度分析】
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
1568. 使陆地分离的最少天数
【问题描述】
给你一个由若干 0 和 1 组成的二维网格 grid
,其中 0 表示水,而 1 表示陆地。岛屿由水平方向或竖直方向上相邻的 1 (陆地)连接形成。
如果 恰好只有一座岛屿 ,则认为陆地是 连通的 ;否则,陆地就是 分离的 。
一天内,可以将任何单个陆地单元(1)更改为水单元(0)。
返回使陆地分离的最少天数。
提示:
1 <= grid.length, grid[i].length <= 30
grid[i][j] 为 0 或 1
【注意点&思路】
简化问题 + BFS
当时由于前两题耗了太多时间,稍微看了一眼就被吓到了,后面看了大佬们的解析后才发现这题是纸老虎。因为每过一天将一个1变为0,而任何连通的陆地模块,必有一个陆地(位于)最多与两个陆地相连,这意味着最多经过2天就能使陆地分离,因此该题的答案只可能为0,1,2。
这样的话,我们可以先记录陆地总个数和其中一个位置,从这个位置向四周扩展,对所有陆地进行层序遍历,标记所有访问过的陆地块,避免死循环;最后看遍历完后的陆地个数是否与总陆地个数相等,如果不相等则说明有分离的陆地。
至于分离经历的天数,首先对原数组进行上述的过程,看是否分离,如果分离则天数为0, 要使天数为1,就要逐个陆地地取掉,然后检查是否分离,到下一轮循环前还要复原;如果所有陆地都取掉过但没有满足上述条件,则返回2。
【代码】
class Solution {
public:
int n,m;
static constexpr int dx[4] = {
-1, 0, 1, 0};
static constexpr int dy[4] = {
0, 1, 0, -1};
bool checkSeparate(const vector<vector<int>>& grid){
int cnt=0;
int x=-1, y=-1;
for (int i=0; i<n; ++i){
for (int j=0; j<m; ++j){
if(grid[i][j] == 0)
continue;
cnt++;
x = i, y = j;
}
}
if(cnt == 0)
return true;
queue<pair<int, int>> que;
que.emplace(x, y);
bool visited[30][30] {
0};
visited[x][y] = true;
while (!que.empty()){
auto p = que.front();
que.pop();
cnt--;
for (int i=0; i<4; ++i){
int mx = p.first+dx[i];
int my = p.second+dy[i];
if(mx<0 || mx >= n || my<0 || my>=m || grid[mx][my] == 0 || visited[mx][my])
continue;
visited[mx][my] = true;
que.emplace(mx, my);
}
}
return cnt != 0;
}
int minDays(vector<vector<int>>& grid) {
n = grid.size();
m = grid[0].size();
if(checkSeparate(grid))
return 0;
for (int i=0; i<n; ++i){
for (int j=0;j<m; ++j){
if(grid[i][j] == 0)
continue;
grid[i][j] = 0;
if(checkSeparate(grid))
return 1;
grid[i][j] = 1;
}
}
return 2;
}
};
【复杂度分析】
时间复杂度: O ( n 4 ) O(n^{4}) O(n4)
空间复杂度: O ( n 2 ) O(n^{2}) O(n2)
1569. 将子数组重新排序得到同一个二叉查找树的方案数
问题链接
这道题当时也就瞥了一眼,没时间做了。后面又是看了大佬的思路才理解的。
【问题描述】
给你一个数组 nums
表示1
到 n
的一个排列。我们按照元素在 nums
中的顺序依次插入一个初始为空的二叉查找树(BST)。请你统计将 nums
重新排序后,统计满足如下条件的方案数:重排后得到的二叉查找树与 nums
原本数字顺序得到的二叉查找树相同。
比方说,给你 nums = [2,1,3]
,我们得到一棵 2 为根,1 为左孩子,3 为右孩子的树。数组 [2,3,1]
也能得到相同的 BST,但 [3,2,1]
会得到一棵不同的 BST 。
请你返回重排 nums
后,与原数组 nums
得到相同二叉查找树的方案数。
由于答案可能会很大,请将结果对 10^9 + 7
取余数。
提示:
1 <= nums.length
<= 1000
1 <= nums[i]
<= nums.length
nums 中所有数 互不相同 。
【注意点&思路】
组合 + 递归
本题只要求个数,注意到数组中第一个元素必为根节点,之后的元素比根节点值小的构成其左子树,大的构成其右子树,假设左子树、右子树元素分别放在less、greater容器中,要使得排列构成的BST与原排列构成的BST一致,只需要确保less、greater中元素的先后顺序一致即可,因此对于已经排好序的左右子树,其可行方案数有 C l e s s . s i z e ( ) + g r e a t e r . s i z e ( ) l e s s . s i z e ( ) C_{less.size() + greater.size()}^{less.size()} Cless.size()+greater.size()less.size()个,而其左右子树各自又可以作为一个BST排序,因此对于有n个结点的BST来说,一共有 F ( n ) = C n − 1 k × F ( k ) × F ( n − k ) F(n) = C_{n-1}^{k} \times F(k) \times F(n-k) F(n)=Cn−1k×F(k)×F(n−k),其中 k k k为其左子树元素总数。
因此,现在的问题主要是组合数的获取,组合数有一个重要性质: C n m = C n − 1 m + C n − 1 m − 1 C_{n}^{m} = C_{n-1}^{m} + C_{n-1}^{m-1} Cnm=Cn−1m+Cn−1m−1,根据这个公式把题目所给范围内的组合数按上下标从小到大的顺序求出来即可。
另外,注意到组合数、方案数可能溢出,因此每一步加法或乘法运算时都需要取余。其实,任何可能溢出的运算步骤都应该取余,这样得到的答案不会错。
【代码】
class Solution {
public:
int comb[1001][1001] {
0};
static const int mod = 1e9 + 7;
int getCombine(int n, int m){
return comb[n][m];
}
int dfs(const vector<int>& nums){
int size = nums.size();
if(size <= 2){
return 1;
}
vector<int> less, greater;
for (int i=1; i<size; ++i){
if(nums[i] < nums[0])
less.push_back(nums[i]);
else
greater.push_back(nums[i]);
}
int ans = (1LL * getCombine(size-1, less.size()) * dfs(less) % mod)
* dfs(greater) % mod;
return ans;
}
int numOfWays(vector<int>& nums) {
//求所有的组合数
comb[1][0] = comb[1][1] = 1;
for (int i=2; i<=1000; ++i){
comb[i][0] = comb[i][i] = 1;
for (int j=1; j<i; ++j){
comb[i][j] = (1LL * comb[i-1][j-1] + comb[i-1][j]) % mod ;
}
}
return dfs(nums)-1;
}
};
【复杂度分析】
时间复杂度: O ( n 2 ) O(n^{2}) O(n2),组合数获取的复杂度为 O ( n 2 ) O(n^2) O(n2),递归的复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),相加可得时间复杂度为 O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n 2 ) O(n^2) O(n2),组合数的存储占用空间 O ( n 2 ) O(n^2) O(n2),dfs函数递归开销为 O ( H × n ) O(H \times n) O(H×n),其中 H H H为递归深度,即BST的高度,最坏情况下H趋于n,即最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2),故综合考虑其空间复杂度为 O ( n 2 ) O(n^2) O(n2)
以上便是本次周赛的复盘,写下这篇复盘时后面两题还没做,也没思路。通过这次认真总结解题方法,不仅仅让我对做出的题目有了更清晰的思路,也为我下次做难题作了算法储备,扩宽了解题手段,过程中还训练了对于难题的复杂度分析。综上,这次复盘可谓收获满满,明天就是全新的一个月,继续加油吧~