提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
问题
给定一个无序数组arr,其中元素可正、可负、可0。给定一个整数k,求arr所有的子数组中累加和小于或等于k的最长子数组长度
例如:arr = [3, -2, -4, 0, 6], k = -2. 相加和小于等于-2的最长子数组为{3, -2, -4, 0},所以结果返回4
输入描述:
第一行两个整数N, k。N表示数组长度,k的定义已在题目描述中给出 第二行N个整数表示数组内的数
输出描述:
输出一个整数表示答案
备注
一、问题分析
首先最容易想到的就是暴力,遍历每一个arr[i]求其往后累加满足条件(和小于等于K)的长度,再从每一个arr[i]对应的长度中求最大的,这种必定超时。所以,我们要充分利用每一步能够去记录的值,通过每一步多做一点事情来优化循环的总次数。
二、二分查找+前缀和O(nlgn)
用前缀和+二分查找很好理解,寻找存在满足条件的最长子数组核心是:找到>= sum − k 最早出现的位置。题目转换为:在长度为 n 的数组中,快速找到最早的 >= sum − k 的位置,
其思路是:
1、用sum{i}表示遍历到当前位置的前缀和
2、建立升序数组(目的是为了用二分查找)help[i]升序数组表示从头开始到当前位置i最大累加和
3、如果当前位置累加和为sum{i} 而如果 i 前面某个位置 j 满足help[j]>=sum{i}-k的话,那么从位置j+1到 i 这个区间的和一定小于等于K ,此时记录这个长度 len = i - j ,也就是遍历到当前位置前面满足条件的长度,最后在求所有满足条件中最长的那个一个就是答案
4、为什么要求最大累加和数组help[i] 而不是普通累加和 ?再次提醒因为要用二分查找者也是建立升序数组 help 的目的所在 ,另外注意是在找某个位置 j (满足 help[j]>=sum{i}-k) 肯定是找第一次满足条件的位置。如果你到这还没有自动理解为什么要找第一次满足条件的位置,那么请重新仔细阅读此方法的原理 。因为数组 help 里面记录从头到当前位置 最大累加和 help有重复元素 ,假设 help 有序列{.........a,a,a,a,a........}在每一组重复元素中最前面(也就是第一次出现的位置)才是严格从开始位置到这个位置累加和为 a 的位置
5、核心找出第一次满足条件的位置 (help[j]>=sum{i}-k) 用二分查找
代码如下:
int find_lower_bound(int a[],int end,int val){
int l=0,right=end,mid;
while(l<right){
mid=(l+right)>>1;
if(a[mid]>=val){
right=mid;
}else{
l=mid+1;
}
}
return l;
}
void fun1(int n,int k){
int sum = 0;
int val, ret = 0;
for ( int i = 1; i <= n; ++i ) {
scanf("%d", &val);
sum += val;
int p = find_lower_bound(help, i, sum - k );//使用二分查找找到第一次大于等于sum-k的位置
ret = max( ret, i - p );
help[i] = max( sum, help[i - 1] ); //因为要使用二分查找 所以建立升序数组help
} // help[i]表示以i结尾前面累加最大和
printf("%d\n", ret);
}
三、双指针错误解法(漏洞解法)
在网上发现有作者说:计算以当前元素作为结尾最长的连续子数组长度,但是使用另外一种不消耗内存的方法,即双指针法。简单地说就是固定左指针,动右指针,当两指针之间的元素和大于了给定数,则移动右指针,直到两指针内部元素之和小于或等于了给定数为止
void fun2(int n,int s){
int num = 0;
int sum = a[1];
int i = 1;
int j = 2;
for(int _i=1;_i<=n;_i++)
{
//找到第一个小于S的下标 计为i
if(a[_i]<=s) //找到第一个小于S的数记为sum 长度num=1
{
i = _i;
j = _i+1;
sum = a[_i];
num = 1;
break;
}
}
while (j <= n) {
if (sum + a[j] <= s) {
sum += a[j++];
num = max(num, j - i);
} else {
sum += a[j++];
while (i <= j && sum > s) {
sum -= a[i];
i++;
}
}
}
printf("%d", num);
}
这种解法开头先初始化第一个位置为小于K的位置 ,这个没有错 但错误就出现在while里面
while (j <= n) {
if (sum + a[j] <= s) {
sum += a[j++];
num = max(num, j - i);
} else {
sum += a[j++];
while (i <= j && sum > s) {
sum -= a[i];
i++;
}
}
}
当满足条件 右指针向后移动 但每次移动一个单位导致一旦出现加上sum+a[j] >s 而 加上 a[j] 后面一个元素满足条件sum+a[j+1] <=s 时 ,这时候忽略了sum+a[j+k] <=s 而移动左指针 ,也就是说一旦碰到加上这个元素就不满足条件sum+a[j]<=s 就移动了左指针
正确应该输出 6(第二行是正解) 而错误解法输出4(第三行) 原因就是 {0+2+5-3+4}=8>给定的 7 而移动了左指针 实际上
{0+2+5-3+4-2}=6<7 满足条件应该此时输出的长度为 6
四 、史诗级O(n)滑动窗口思想
先附上图片
光看图片好像效果像暴力遍历一样 其实不是
1、我们建立数组 h[i] 表示从 i 位置往后遍历最小和为 h[i]
2、 ends[i] 表示从i 位置往后加到最小和的结束位置 ends[i]
3、从前往后遍历 第一次从头开始借助h[i] 和 ends[i] 来找到最远位置 长度len=ends[i]+1-i 第一次结束位置即不满足条件的位置 此时可以像解法二那样移动左指针 与解法二的区别就是我们的右指针不是一个一个地移动 而是借助提前算好的最小累加和 h[i] 来计算满足条件的结束位置
4、第一次找到了最远满足条件(累加和小于等于K)的位置 这时候 向右滑动窗口 右边继续向右延申 而左边弹出元素 直到右边到达边界为止
注意特殊情况
1、end如果没有移动 也就意味着此时sum加上当前位置往后最小累加和没有满足条件,
sum就没有加上这个元素 因此也不需要弹出左边的 因为左边的压根没加进sum
2、如果进了while end就从end加一开始 如果开始就没有延申出去 end就从i+1开始
代码:
void fun2(int n,int k){ //滑动窗口思想
// special case
if (n < 1)
return ;
int arr[n];
for (int i=0; i<n; i++){
cin >> arr[i];
}
int h[n], ends[n];
h[n-1] = arr[n-1];
ends[n-1] = n-1;
// 找到从i出发的最长的最小值子队列
for(int i=n-2; i>-1; i--){
if(h[i+1]<=0){
h[i] = arr[i] + h[i+1];
ends[i] = ends[i+1];
}
else{
h[i] = arr[i];
ends[i] = i;
}
}
// 从前往后,依次寻找和比较从位置i出发,满足要求小于等于k的子数组并比较长度
int len=0, end=0, sum=0;
for(int i=0; i<n; i++){
while(end<n && (sum+h[end]<=k)){
sum += h[end];
end = ends[end]+1;
}
len = max(len, end-i); // 注意上面的循环退出条件,这里end实际上已经在符合条件的区间外了,所以无需再+1
sum -= end > i ? arr[i]: 0; // 同样注意上面的循环条件,如果end压根没动,sum值则未更新,不需要进行再次清零操作
end = max(i+1, end); // 同理,此时end恰好在边界外,无需+1
}
cout << len;
}
总结
多想 多思考 多琢磨 多抽象成过程
链接:未排序数组中累加和小于或等于给定值的最长子数组长度__牛客网
来源:牛客网