LeetCode第204场周赛
本场周赛题目位于这里
本次题目感觉还是比较有趣的,不是繁琐的业务或者单纯的技巧,注重思考的过程。
1· 重复至少 K 次且长度为 M 的模式
给你一个正整数数组 arr,请你找出一个长度为 m 且在数组中至少重复 k 次的模式。
模式 是由一个或多个值组成的子数组(连续的子序列),连续 重复多次但 不重叠 。 模式由其长度和重复次数定义。
如果数组中存在至少重复 k 次且长度为 m 的模式,则返回 true ,否则返回 false 。
示例 1:
输入:arr = [1,2,4,4,4,4], m = 1, k = 3
输出:true
解释:模式 (4) 的长度为 1 ,且连续重复 4 次。注意,模式可以重复 k 次或更多次,但不能少于 k 次。
示例 2:
输入:arr = [1,2,1,2,1,1,1,3], m = 2, k = 2
输出:true
解释:模式 (1,2) 长度为 2 ,且连续重复 2 次。另一个符合题意的模式是 (2,1) ,同样重复 2 次。
提示:
2 <= arr.length <= 100
1 <= arr[i] <= 100
1 <= m <= 100
2 <= k <= 100
解析
第一题又是例行的签到题。题目给定一个数组,要求我们找到是否存在连续重复k次的子数组,其中重复的单元长为m。
那么本题显然,给定的数组最少长度为
m
∗
k
m*k
m∗k,否则根本不可能存在,元素数量就不够。那么接下来就要看是否存在连续重复的了。我们记这个最少长度为
m
i
n
L
=
m
∗
k
minL = m*k
minL=m∗k,数组总长度为
l
e
n
len
len,那么这一段重复的模式起始位置,可以从0开始,但最靠后的起始位置应该是
l
e
n
−
m
i
n
L
e
n
len - minLen
len−minLen,从更靠后的位置开始,元素数量不足以支持k个模式的重复。
那么我们定义起始下标
i
i
i,在范围
0
<
=
i
<
=
l
e
n
−
m
i
n
L
e
n
0<=i<=len - minLen
0<=i<=len−minLen内枚举所有的
i
i
i,以及对应的长度为m的子数组,作为潜在可能的重复模式。此时计数器为1,即存在1个模式。
接着定义搜索下标
j
j
j从
i
+
m
i+m
i+m处开始不断向后搜索,计数器count小于我们要求的次数k时,在不越界的情况下检查从
j
j
j开始的m个元素是否是所枚举的潜在模式的重复,如果是,就给计数器加1,表明又找到一个连续的重复模式。此时如果count达到k,意味着满足题意,存在这样的重复模式,返回true。否则,就继续移动
j
j
j寻找下一个模式。
一旦搜索过程中出现对应位置不等,意味着当前枚举的模式不存在k个连续的重复,更换下一个潜在的模式,继续搜索。如果所有的模式均不满足,则返回false。
C++代码如下:
//No 1
bool containsPattern(vector<int>& arr, int m, int k) {
int len = arr.size(),minLen=m*k;
if (len < minLen) return false;
for (int i = 0; i <= len - minLen; ++i) {
vector<int>tmp(arr.begin() + i, arr.begin() + i + m);
int count = 1;
int j = i + m;
bool nn=true;
while (count < k && j < len) {
int t = 0;
while (j < len && t<m) {
if (tmp[t] == arr[j]) {
++t;
++j;
}
else {
nn = false;
break;
}
}
if (t == m) ++count;
if (j == len || !nn) break;
}
if (count == k) return true;
}
return false;
}
本题数据范围较小,不会超时。但要注意下标以及循环跳出的细节。
2· 乘积为正数的最长子数组长度
给你一个整数数组 nums ,请你求出乘积为正数的最长子数组的长度。
一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。
请你返回乘积为正数的最长子数组长度。
示例 1:
输入:nums = [1,-2,-3,4]
输出:4
解释:数组本身乘积就是正数,值为 24 。
示例 2:
输入:nums = [0,1,-2,-3,-4]
输出:3
解释:最长乘积为正数的子数组为 [1,-2,-3] ,乘积为 6 。
注意,我们不能把 0 也包括到子数组中,因为这样乘积为 0 ,不是正数。
提示:
1 <= nums.length <= 10^5
-10^9 <= nums[i] <= 10^9
解析
本题给定一个数组,让我们找到其中乘积为正数的子数组的最大长度。
由于数组元素有正、有负、有零,我们需要考虑:
1· 0乘任何数都是0,所以包含元素值为0的子数组乘积是0而不是正数
2· 子数组乘积为正数可能是正X正或负X负
本题显然是一道动态规划,因为最长的子数组必然以数组中某元素做结尾,我们只要知道每个元素结尾时能得到的最长正数乘积子数组的长度,再寻找最大值即可。而一个数结尾时的子数组长度,显然可以由它的前一个数以及正负关系得到。
我们定义两个dp数组,分别记作
p
o
s
,
n
e
g
pos, neg
pos,neg,其中
p
o
s
[
i
]
,
n
e
g
[
i
]
pos[i],neg[i]
pos[i],neg[i]分别代表以下标为
i
i
i的元素结尾时最长的乘积为正数的子数组长度,与最长的乘积为负数的子数组长度。状态转移方程如下:
- n u m s [ i ] = = 0 : p o s [ i ] = n e g [ i ] = 0 nums[i]==0: pos[i]=neg[i]=0 nums[i]==0:pos[i]=neg[i]=0
-
n
u
m
s
[
i
]
>
0
:
nums[i]>0:
nums[i]>0:
p o s [ i ] = p o s [ i − 1 ] > 0 ? p o s [ i − 1 ] + 1 : 1 pos[i] =pos[i-1]>0? pos[i-1]+1:1 pos[i]=pos[i−1]>0?pos[i−1]+1:1
n e g [ i ] = n e g [ i − 1 ] > 0 ? n e g [ i − 1 ] + 1 : 0 neg[i]=neg[i-1]>0?neg[i-1]+1:0 neg[i]=neg[i−1]>0?neg[i−1]+1:0 -
n
u
m
s
[
i
]
<
0
:
nums[i]<0:
nums[i]<0:
p o s [ i ] = n e g [ i − 1 ] > 0 ? n e g [ i − 1 ] + 1 : 0 pos[i] =neg[i-1]>0? neg[i-1]+1:0 pos[i]=neg[i−1]>0?neg[i−1]+1:0
n e g [ i ] = p o s [ i − 1 ] > 0 ? p o s [ i − 1 ] + 1 : 1 neg[i]=pos[i-1]>0?pos[i-1]+1:1 neg[i]=pos[i−1]>0?pos[i−1]+1:1
首先,当元素值为0时,以当前元素结尾的所有子数组乘积都是0,没有正的也没有负的,所以此时 p o s [ i ] , n e g [ i ] pos[i],neg[i] pos[i],neg[i]都是0;
当元素是正数时,首先讨论以该元素结尾乘积为正数的情况,如果前一个数 i − 1 i-1 i−1处的 p o s [ i − 1 ] > 0 pos[i-1]>0 pos[i−1]>0,也就是说以前一个元素结尾的乘积为正数的最长子数组是存在的,那么只要把当前数乘上去,就可以得到以当前数结尾的乘积为正数的最长子数组,等于在前一个的基础上扩充一个元素;反之如果 p o s [ i − 1 ] = = 0 pos[i-1]==0 pos[i−1]==0意味着以前一个数结尾没有乘积为正数的子数组,那当前数只能另立门户,自己做一个子数组,并且是正数,此时长度为1;再讨论 n e g [ i ] neg[i] neg[i]的更新,如果 n e g [ i − 1 ] > 0 neg[i-1]>0 neg[i−1]>0那么继续把当前这个数乘上去,结果依然是负数,等于在前一个结尾的数组上扩充,反之,如果不存在前一个数结尾的乘积为负的最长子数组,当前数也不是负数,所以 n e g [ i ] neg[i] neg[i]只能为0.
当元素是负数时,与之前结果相乘符号会反过来,所以参照正数的情况,但考虑的正负性相反即可。
遵循上述状态转移方程,代码如下:
//No 2
int getMaxLen(vector<int>& nums) {
int maxL = 0, len = nums.size();
vector<int>pos(len, 0), neg(len, 0);
if (nums[0] > 0) pos[0] = 1;
else if (nums[0] < 0) neg[0] = 1;
maxL = max(maxL, pos[0]);
for (int i = 1; i < len; ++i) {
if (nums[i] == 0) {
pos[i] = 0;
neg[i] = 0;
}
else if (nums[i] > 0) {
if (pos[i - 1] > 0) pos[i] = pos[i - 1] + 1;
else pos[i] = 1;
if (neg[i - 1] > 0)neg[i] = neg[i - 1] + 1;
else neg[i] = 0;
}
else {//nums[i] < 0
if (neg[i - 1] > 0) pos[i] = neg[i - 1] + 1;
else pos[i] = 0;
if (pos[i - 1] > 0)neg[i] = pos[i - 1] + 1;
else neg[i] = 1;
}
maxL = max(maxL, pos[i]);
}
return maxL;
}
3· 使陆地分离的最少天数
给你一个由若干 0 和 1 组成的二维网格 grid ,其中 0 表示水,而 1 表示陆地。岛屿由水平方向或竖直方向上相邻的 1 (陆地)连接形成。
如果 恰好只有一座岛屿 ,则认为陆地是 连通的 ;否则,陆地就是 分离的 。
一天内,可以将任何单个陆地单元(1)更改为水单元(0)。
返回使陆地分离的最少天数。
示例 1:
输入:grid = [[0,1,1,0],[0,1,1,0],[0,0,0,0]]
输出:2
解释:至少需要 2 天才能得到分离的陆地。
将陆地 grid[1][1] 和 grid[0][2] 更改为水,得到两个分离的岛屿。
示例 2:
输入:grid = [[1,1]]
输出:2
解释:如果网格中都是水,也认为是分离的 ([[1,1]] -> [[0,0]]),0 岛屿。
提示:
1 <= grid.length, grid[i].length <= 30
grid[i][j] 为 0 或 1
解析
本题给定一个二维数组,元素只包含0和1,0代表水,1代表陆地。1与其四领域的其他1是连通的,视为一块陆地。题目要求我们每天能够将一个1改为0,问最短多少天可以将原有的陆地分离,也就是至少存在2块不连通的陆地。
本题乍一看非常复杂,既涉及到搜索判断陆地连通性和块数,又需要想怎么样最短时间的分离陆地。实际上我们思考一下,就会发现并不需要如此复杂。
我们想象一下最差情况怎么样分离陆地。假设一块全是1的区域
如何分离?显然题目对分完后的大小无任何要求,那么一定存在2步的分割方法。如上图所示,我们分别将标红的两个格子变为0,那么左上角的1就不与其他陆地相连了。事实上其他情况也存在这样的特点,我们一定能找到这样的“角块”,比如左上角(定义为其上方与左方均是边界或0)那么最多通过2步就可以将这个角块分离出去。
因此,本题的答案只可能是0,1,或2三种情况。或者说,不能在0或1天解决的情况,一律返回2就够了,因为一定存在这样的解法。
所以,我们首先定义一个函数,用于计算当前情况下存在几个1的区域,这些区域又能连成几个陆地。几个1只要遍历所有格子累加就行,连通的陆地个数,我们通过dfs求解。首先初始化一个数组visited记录每个点是否被访问过,接着遍历所有的点,当遇到一个值为1并且未被访问的点,就从这个点开始,递归的深度优先搜索相邻的1,被访问过就记录在visited中,递归中返回时就意味着这一片连通的1已经都访问过了,这样可以统计到总的陆地数量。
首先,我们对初始给定的grid二维数组计算其陆地与1的数量,如果出现陆地块数为0或大于1的情况,意味着当前没有陆地或已经分离了,不需要任何操作,直接返回0就可以了,一天时间都不需要。
接下来讨论是否能在1天分离好。实际上我们很难想到一个通用的策略表征哪个位置优先消除可以最短分离陆地,但事实上也不需要这么做。由于输入矩阵的大小是30*30的,数据规模很小。我们遍历寻找陆地数量,需要遍历每个点,也即 O ( N 2 ) O(N^2) O(N2)的复杂度。我们可以尝试将每一个的1位置逐个暂时的修改为0,去考察修改后的部分是不是分离好,如果是,那意味着改这个位置就能1天搞定,反之,就将它改回为1,再去看修改下一个1位置能不能做到。实际上遍历所有1位置,将其修改为0,在查看陆地块数,复杂度也就是 O ( N 4 ) O(N^4) O(N4),对于30这个数量级完全不会超时。
那么如果输入直接满足题意(返回0天),或存在改一个位置就满足(返回1天),如果不属于上述两种,那只能是第三种情况,2天,返回2即可。
C++代码如下:
//No 3
void dfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int r, int c, int m, int n) {
if (grid[r][c] == 1 && !visited[r][c]) {
visited[r][c] = true;
if (r - 1 >= 0 && grid[r - 1][c] == 1 && !visited[r - 1][c]) dfs(grid, visited, r - 1, c, m, n);
if (r + 1 < m && grid[r + 1][c] == 1 && !visited[r + 1][c]) dfs(grid, visited, r + 1, c, m, n);
if (c - 1 >= 0 && grid[r][c-1] == 1 && !visited[r][c-1]) dfs(grid, visited, r, c-1, m, n);
if (c+1 <n && grid[r][c+1] == 1 && !visited[r][c+1]) dfs(grid, visited, r, c+1, m, n);
}
return;
}
vector<int>countLand(vector<vector<int>>& grid, int m, int n) {
vector < vector<bool>>visited(m, vector<bool>(n, false));
int land = 0, count = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == 1) {
++count;
if (!visited[i][j]) {
++land;
dfs(grid, visited, i, j, m, n);
}
}
}
}
return { land,count };
}
int minDays(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<int>c = countLand(grid, m, n);
int land = c[0], count = c[1];
if (land > 1||land==0) return 0;
if (count == 1) return 1;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == 1) {
grid[i][j] = 0;//修改为0,看能不能分离
c = countLand(grid, m, n);
land = c[0];
count = c[1];
if (land > 1 || land == 0) return 1;
grid[i][j] = 1;//记得改回来,因为1天只能修改一处
}
}
}
return 2;
}
4· 将子数组重新排序得到同一个二叉查找树的方案数
给你一个数组 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 = [2,1,3]
输出:1
解释:我们将 nums 重排, [2,3,1] 能得到相同的 BST 。没有其他得到相同 BST 的方案了。
示例 2:
输入:nums = [3,4,5,1,2]
输出:5
解释:下面 5 个数组会得到相同的 BST:
[3,1,2,4,5]
[3,1,4,2,5]
[3,1,4,5,2]
[3,4,1,2,5]
[3,4,1,5,2]
解析
本题是给定了一个数组,长度为你,包含元素1-n。让我们从一颗空的二叉搜索树(BST)开始,将数组元素逐个填进去,这样构造出一颗BST。显然,如果一定程度上调换数组元素顺序,也可能存在这样的调换方法(或称为这些数的一个排列)使得重建出的BST与原数组一致。问我们这样的重排方案有多少个。注意这里的重排,是指不同于原数组的。
本题看起来比较绕,我们举例说明:
比如上述实例2:
给定数组为nums = [3,4,5,1,2],显然第一个元素3 填在根节点,那么1, 2一定位于左子树,4,5 一定位于右子树,再往后先遇到了4,意味着4是3的右子节点,5只能放在4的右子节点。注意这里如果5出现早于4,那一定是5在3的右子节点,跟原来的BST就不一样的;左子树也是这样,先1后2.所以相同的重排一定满足:
3必须第一个,否则根节点就不一样;
4一定在5前面,1一定在2前面;但是左右子树的元素并没有严格的顺序。
换句话说,相同重排的要求是:根节点必须一样,左子树出现的内部顺序要一样,右子树出现的内部顺序一样,但是左右两组可以乱序。那么两组数,每组两个,组内有顺序的排列就是C(m+n,n)=C(4,2)=6.当然题目给定的排列也在其中,要去掉,所以是6-1=5。
这个例子因为层数很少所以较为容易,那么元素和层数很多的时候如何处理呢?答案是递归来做。
根节点固定,那么我们就能够将剩下元素分为左右子树两个部分,我们递归的求左右子树各自有多少种排列方式,即为
l
,
r
l,r
l,r,对于每一个确定的左右子树方式,合起来的时候都有C(l+r,l)中方法,那么总计
l
∗
r
∗
C
(
l
+
r
,
l
)
l*r*C(l+r,l)
l∗r∗C(l+r,l)种结果。最后减1就是除输入外的排列方式数。
这个组合数的计算每次重算较为繁琐,浪费时间,可以用空间换时间,先生成给所有组合数的表,对于数据规模n来说,这张表大小为n*n。
在递归函数中,传入一个数组,返回这个数组相同BST的排列数。对于空数组,只有空树一种,返回1.否则就记录根节点,分割左右子树,递归求解左右子树,在套用上述的
l
∗
r
∗
C
(
l
+
r
,
l
)
l*r*C(l+r,l)
l∗r∗C(l+r,l)即可。
要注意取余否则会溢出。
递归子函数返回的是全部排列数,包括当前的输入对应的这个排列。而主函数最后返回的时候要-1,因为题目要求除去输入这一种。
C++代码如下:
//No 4
const int M = 1e9 + 7;
vector<vector<int>>combine;
int func(vector<int>& nums) {
if (nums.empty())return 1;
int n = nums.size();
int k = nums[0];
vector<int>left, right;
for (auto i : nums) {
if (i < k)left.push_back(i);
else if (i > k)right.push_back(i);
}
return (int64_t)combine[n - 1][left.size()] * func(left) % M * func(right) % M;
}
int numOfWays(vector<int>& nums) {
int n = nums.size();
combine = vector<vector<int>>(n + 1, vector<int>(n + 1));
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= i; ++j) {
if (!j)combine[i][j] = 1;
else combine[i][j] = (combine[i - 1][j - 1] + combine[i - 1][j]) % M;
}
}
return (func(nums) + M - 1) % M;
}