检测相邻递增子数组1 2(LeetCode 3349 3350)
1 介绍
本文介绍检测相邻递增子数组1 & 2的思路以及解法。
2 检测相邻递增子数组1
2.1 原题

2.2 思路
由于范围不大,数组长度只有100,可以直接考虑暴力算法。
具体来说,就是遍历数组中的每个下标i,然后再遍历一次[i,i+k)和[i+k,i+2*k),需要两个子数组都是严格递增的。
代码如下,带有详细注释。
class Solution {
public:bool hasIncreasingSubarrays(vector<int> &nums, int k) {// i表示当前遍历的下标// i的范围是[0,n-2*k],因为当i=n-2*k时,i+2*k刚好是最后一个,注意取值不会取到i+2*k,只会取到i+2*k-1for (int i = 0, n = static_cast<int>(nums.size()); i + 2 * k <= n; ++i) {// 表示是否严格递增bool valid = true;// 判断第一个数组是否严格递增for (int j = i + 1; j < i + k; ++j) {if (nums[j] <= nums[j - 1]) {// 如果不是严格递增,记录false,然后跳过剩下的直接breakvalid = false;break;}}// 如果不是严格递增,从下一个下标开始处理,直接continueif (!valid) {continue;}// 第二个数组处理类似,只不过范围变成[i+k,i+2*k)for (int j = i + k + 1; j < i + 2 * k; ++j) {if (nums[j] <= nums[j - 1]) {valid = false;break;}}if (!valid) {continue;}// 如果找到了,直接返回truereturn true;}return false;}
};
2.3 Java版本
public class Solution {public boolean hasIncreasingSubarrays(List<Integer> nums, int k) {for (int i = 0, n = nums.size(); i + 2 * k <= n; i++) {boolean valid = true;for (int j = i + 1; j < i + k; j++) {if (nums.get(j) <= nums.get(j - 1)) {valid = false;break;}}if (!valid) {continue;}for (int j = i + k + 1; j < i + 2 * k; j++) {if (nums.get(j) <= nums.get(j - 1)) {valid = false;break;}}if (!valid) {continue;}return true;}return false;}
}
2.4 Go版本
func hasIncreasingSubarrays(nums []int, k int) bool {for i, n := 0, len(nums); i+2*k <= n; i++ {valid := truefor j := i + 1; j < i+k; j++ {if nums[j] <= nums[j-1] {valid = falsebreak}}if !valid {continue}for j := i + k + 1; j < i+2*k; j++ {if nums[j] <= nums[j-1] {valid = falsebreak}}if !valid {continue}return true}return false
}
3 检测相邻递增子数组2
3.1 原题

3.2 思路
题目描述不同的是需要求最长的k,并且范围扩大了,变成了2 * 10^5。
在第一题的思路中,可以发现其实有非常多的无效计算,导致了时间复杂度是O(n^2)。
实际上,在每次计算的时候,可以算出从当前下标开始,有多少个值是连续严格递增的。
例如例子中的[2,5,7,8,9,2,3,4,3,1],可以手动计算一下:
- 下标为
0,从2开始,只有2个数连续递增 - 下标为
1,从5开始,只有1个数连续递增(1个数也算) - 下标为
2,从7开始,只有3个数连续递增 - 下标为
3,从8开始,只有2个数连续递增 - 下标为
4,从9开始,只有1个数连续递增 - 下标为
5,从2开始,只有3个数连续递增 - 下标为
6,从3开始,只有2个数连续递增 - 下标为
7,从4开始,只有1个数连续递增 - 下标为
8,从3开始,只有1个数连续递增 - 下标为
9,从1开始,只有1个数连续递增
可以看到,从下标2和下标5开始,都有3个数连续递增,并且两个子数组是相邻的,以及此时的k最大,所以答案就是3。
所以这里的思路就是计算“有几个数连续递增”,这个很容易做到,在一边遍历数组的时候一边赋值即可,使用一个叫cnt的数组存储这些值。
另一方面,最长递增的相邻数组有两种情况:
- 第一种情况是像例子中的一样,两个子数组拼在一起不是连续递增的,例如
[7,8,9,2,3,4] - 第二种情况是,两个子数组拼在一起连续递增,这样的话长度就是取一半,例如
[1,2,3,4,5,6],这样的话k=3
所以,得出cnt数组之后,对cnt数组进行如下处理:
- 遍历数组每一个下标
i - 判断是否是第一种情况,可以通过
i+cnt[i] < n && cnt[i] <= cnt[i+cnt[i]]判断 - 判断是否是第二种情况,可以通过
i+cnt[i]/2 < n && cnt[i]/2 <= cnt[i+cnt[i]/2]判断 - 如果符合其中一种,计算当前的值并与结果值比较,最后返回最大值
这里解释一下第一条式子:
i+cnt[i] < n:由于位于i时,最长能取到的递增长度是cnt[i],而i+cnt[i]就是下一个相邻下标的值,这个值需要在数组范围内,也就是i+cnt[i]<ncnt[i] <= cnt[i+cnt[i]]:cnt[i]是当前能取到的最大的值,而cnt[i+cnt[i]]表示下标i+cnt[i]能取到的最大值,这里的判断意思就是类似[7,8,9,1,2,3,4]这样的情况,其中cnt[0]=3,cnt[3]=4,当i=0时,cnt[0] <= cnt[0+3],也就是cnt[0] <= cnt[3]
第二条式子:
i+cnt[i]/2 < n:位于i时,最长能取到的是cnt[i],这里是处理第二种情况,所以cnt[i]需要取一半,i+cnt[i]/2就是第二个数组的起始下标cnt[i]/2 <= cnt[i+cnt[i]/2]:和cnt[i] <= cnt[i+cnt[i]]类似,只不过值都是除2,一个例子是[1,2,3,4,5,6,7],当i=0时,cnt[0]=7,i+cnt[i]/2为3,cnt[i+cnt[i]/2]为4,符合题目要求,此时k=3
代码如下:
class Solution {
public:int maxIncreasingSubarrays(vector<int> &nums) {const int n = static_cast<int>(nums.size());// cnt数组vector<int> cnt(n);// l是辅助变量,控制上一次的第一个数组的起始位置,用于后面赋值cnt数组int l = 0;// 结果int res = 0;for (int i = 1; i < n; ++i) {// 不断遍历数组,直接遇到非严格递增的if (nums[i] > nums[i - 1]) {continue;}// 起始位置是l,长度cur=i-l,然后赋值给cnt数组for (int j = l, cur = i - l; j < i; ++j) {cnt[j] = cur--;}l = i;}// 兜底处理,用于处理整个数组都是递增数组的情况,如[1,2,3,4,5]// 这种情况下,进入不到上面for循环的处理cnt的部分,l一直是0for (int j = l, cur = n - l; j < n; ++j) {cnt[j] = cur--;}// 遍历cnt数组for (int i = 0; i < n; ++i) {// 第一种情况,公式详解见上面描述if (i + cnt[i] < n && cnt[i] <= cnt[i + cnt[i]]) {// 第一种情况下最大是cnt[i]res = max(res, cnt[i]);} else if (i + cnt[i] / 2 < n && cnt[i] / 2 <= cnt[i + cnt[i] / 2]) {// 第二种情况,最大是cnt[i]/2res = max(res, cnt[i] / 2);}}return res;}
};
3.3 O(1)空间优化
实际上,上面的做法中,可以不存储cnt的值,从而将空间进一步优化到O(1)。
思路就是计算cnt的同时,使用一个变量保存上一次cnt的值:
- 遍历到非递增的时候,保存上一次的
cnt,并重新累计cnt - 遍历到递增的时候,计算当前的最大值
其中“计算当前的最大值”也包含了上面分析的两种情况:
- 第一种情况:值是
min(last_cnt,cnt),因为两者只能取小值 - 第二种情况:值是
cnt/2,分析与上面类似,就是取一半
最终答案就是两者取最大值,max(cnt/2,min(last_cnt,cnt))。
代码如下:
class Solution {
public:int maxIncreasingSubarrays(vector<int> &nums) {// 结果最小是1int res = 1;// 下标从1开始,cnt表示当前递增的长度,last_cnt保存上一个cntfor (int i = 1, cnt = 1, last_cnt = 1, n = static_cast<int>(nums.size()); i < n; ++i) {// 如果遇到非递增的if (nums[i] <= nums[i - 1]) {// 保存上一次cnt的值last_cnt = cnt;// cnt重新计算cnt = 1;continue;}// 遇到递增的,累加cnt++cnt;// 计算最终结果res = max(res, max(cnt / 2, min(last_cnt, cnt)));}return res;}
};
3.4 二分
这种做法可能比较难想,但是实际上是可以的。
对于一道题能否使用二分的做法取处理,核心就是看是否具有单调性。
从上面的做法分析可以知道:
k越大,得到答案的概率越低,例如k=10^5,除非数组是有两个10^5的子数组构成的,否则就不成立k越小,得到答案的概率越高,例如k=2的情况肯定比k=10^5更容易找到子数组符合情况
这样就符合了单调性的条件,所以思路就是直接二分答案k,k的范围是[1,n/2]。
每次二分得到k,然后参考上面的解法,计算是否存在max(cnt/2,min(last_cnt,cnt)) >= k。
代码如下:
class Solution {
public:int maxIncreasingSubarrays(vector<int> &nums) {const int n = static_cast<int>(nums.size());// 二分auto check = [&](int m) -> bool {// cnt初始化为1,last_cnt初始化为0int cnt = 1;int last_cnt = 0;// 从下标1开始for (int i = 1; i < n; ++i) {// 如果没有递增,保存cnt,并且重置cntif (nums[i] <= nums[i - 1]) {last_cnt = cnt;cnt = 1;} else {// 否则累加cnt++cnt;}// 这里参考上面的解法,分别对应第一种情况一和第二种情况// 如果找到>=m的,直接returnif (max(cnt / 2, min(last_cnt, cnt)) >= m) {return true;}}return false;};// k的范围是[1,n/2]int l = 1;int r = n / 2;while (l <= r) {// 二分const int m = (l + r) >> 1;// 如果存在符合条件的子数组,表明可以继续扩大kif (check(m)) {// 扩大kl = m + 1;} else {// 如果没有符合条件的子数组,缩小k的范围r = m - 1;}}return r;}
};
遍历的复杂度是O(n),再加上二分k,k的范围是[1,n/2],所以时间复杂度就是O(n log n)。
所以这个做法是比上面的做法(O(n))要慢的,这里仅仅提供一种二分的思路,空间复杂度的话是一样的,都是O(1)。
遍历做法:

二分做法:

3.5 Java版本
3.5.1 遍历解法
public class Solution {public int maxIncreasingSubarrays(List<Integer> nums) {int res = 1;for (int i = 1, cnt = 1, lastCnt = 0, n = nums.size(); i < n; i++) {if (nums.get(i) <= nums.get(i - 1)) {lastCnt = cnt;cnt = 1;continue;}++cnt;res = Math.max(res, Math.max(cnt / 2, Math.min(lastCnt, cnt)));}return res;}
}
3.6.2 二分解法
public class Solution {private boolean check(int m, List<Integer> nums) {for (int i = 1, cnt = 1, lastCnt = 0, n = nums.size(); i < n; i++) {if (nums.get(i) <= nums.get(i - 1)) {lastCnt = cnt;cnt = 1;} else {++cnt;}if (Math.max(cnt / 2, Math.min(lastCnt, cnt)) >= m) {return true;}}return false;}public int maxIncreasingSubarrays(List<Integer> nums) {int l = 1;int r = nums.size() / 2;while (l <= r) {int m = (l + r) >> 1;if (check(m, nums)) {l = m + 1;} else {r = m - 1;}}return r;}
}
3.6 Go版本
3.6.1 遍历解法
func maxIncreasingSubarrays(nums []int) int {res := 1for i, cnt, lastCnt := 1, 1, 0; i < len(nums); i++ {if nums[i] <= nums[i-1] {lastCnt, cnt = cnt, 1continue}cnt++res = max(res, max(cnt/2, min(lastCnt, cnt)))}return res
}
3.6.2 二分解法
func maxIncreasingSubarrays(nums []int) int {n := len(nums)check := func(m int) bool {for i, cnt, lastCnt := 1, 1, 0; i < n; i++ {if nums[i] <= nums[i-1] {lastCnt, cnt = cnt, 1} else {cnt++}if max(cnt/2, min(lastCnt, cnt)) >= m {return true}}return false}l, r := 1, n/2for l <= r {m := (l + r) >> 1if check(m) {l = m + 1} else {r = m - 1}}return r
}
4 总结
本文介绍了检测相邻递增子数组的两种解法:
- 一种是遍历,时间复杂度
O(n),空间复杂度O(1) - 另一种是二分,时间复杂度
O(n log n),空间复杂度O(1)
尽管二分的做法更慢,但是提供了另一种思路去解决问题。
5 附录
- 检测相邻递增子数组1
- 检测相邻递增子数组2
