0. 前言
在做数组相关的算法题时,连续子区间 是出现频率最高,也是花样最多的一个考点。
本文将通过两道算法题,深度对比处理连续子区间的两大方法:针对最大和的 Kadane 算法 与 针对最小区间长度的滑动窗口。
1. 问题描述
1.1 岁晚可可塔
这道题其实就是求最大连续总和,数组元素有正有负。
1.2 内存块的最短分配
这道题其实就是求符合要求的最短连续长度。
现在有一排连续的内存块,每个内存块的大小不一,由一个正整数数组 nums 表示。现在需要分配一个 连续 的内存区域,使得该区域内所有内存块的大小总和 大于等于 给定的目标值 target。
请找出满足条件的 最短连续子数组 的长度。如果不存在符合条件的子数组,返回 0。
输入:
- 第一行:一个整数
target,目标内存大小。 - 第二行:一个整数
n,代表内存块的总数量。 - 第三行:
n个由空格分隔的正整数,每个正整数代表对应的每个内存块的大小。
输出:
- 一个整数,代表满足条件的 最短连续子数组的长度。
2. 算法逻辑对比
我用一个表格总结了一下 Kadane 算法和滑动窗口的算法逻辑,方便大家根据合适的场景进行选择:
3. 代码实现
3.1 岁晚可可塔代码
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
int main() {
int n;
cin >> n;
string dummy;
getline(cin, dummy);//拿掉n后面的换行符
string line;
getline(cin, line);
stringstream ss(line);//将第二行转换成流,然后一个一个往出拿
long max_sum = -2e7;//最终最大甜度,初始赋个极小值,防止第二行全为负数
long curr_sum = 0;//当前最大甜度
for(int i=0; i<n; i++)
{
long curr;
ss >> curr;//从流里面拿出来一个
//kadane算法核心
//如果前面的加上当前的,和还不如当前的自身大,那么前面全是累赘,直接抛弃,以当前为新的开头
curr_sum = max(curr, curr+curr_sum);
//更新max
max_sum = max(curr_sum , max_sum);
}
cout << max_sum;
return 0;
}
有个需要注意的点,如果甜度全是负数,最大值应该也是负数。
这时,如果 max_sum 的初始值为 0,后面取 max 是就会得到 0,最终结果也就是 0,从而导致错误。
还有,在开头,读取 n 的值之后,用 getline 拿掉 n 后面的换行符很关键,否则就会导致后面本该读取第二行内容的 getline 读到了第一行的换行符,也会导致错误。
然后就是 for 循环中的 Kadane 算法,其大致思路如下:
对于数组中的每个元素 x,我们有两种选择:第一,如果前面和为正的话,把 x 加入前面的子数组。第二,如果前面和为负,就抛弃前面的子数组,从 x 本身重新开始一个新的子数组。
3.2 内存块的最短分配代码
#include <iostream>
#include <string>
#include <sstream>
#include <vector>
using namespace std;
int main()
{
int target, n;
string dummy;
cin >> target;
getline(cin,dummy);//依旧拿掉换行符
cin >> n;
getline(cin, dummy);
string line;
getline(cin, line);
stringstream ss(line);
int left = 0;
int min_len = n+1;//将最小长度初始化为一个较大的值
int sum = 0;
vector<int> nums;//动态数组,存放每个内存块的大小
for(int i=0; i<n; i++)
{
//从流中逐个获取每个内存块大小,并存入数组
int temp;
ss >> temp;
nums.push_back(temp);
}
for(int right = 0; right < n; right++)//right负责向右扩展
{
sum += nums[right];//每遍历到一个元素,就把它加到sum
while(sum >= target)//每当sum已经达到target要求时,开始考虑收缩left,直到sum刚好再次小于target
{
min_len = min(min_len, right-left+1);//进入while循环,一定要先记录最小长度,否则会出错
sum -= nums[left];
left++;//左边界收缩
}
}
if(min_len == n+1) cout << 0;
else cout << min_len;
return 0;
}
这段代码中唯一需要拆解的就是滑动窗口的运作逻辑,如下:
while(sum >= target)//先检查现在的sum够不够
{
//记录,此时sum确实是>=target的,这一刻的长度是满足要求的
min_len = min(min_len, right-left+1);
//尝试缩小,把左边的数减掉
sum -= nums[left];
//left向右挪
left++;
//回到最上面重新检查,不满足就跳出循环
}
关键点就在于我们在执行第 3 步减掉数字之前,已经先在第 2 步把当前的 min_len 存起来了,如果减掉 nums[left] 之后,sum 变得小于 target 了,下一次 while 循环的条件就会判定为 false,循环直接结束,而我们最后一次记录 min_len 的那个时刻,sum 依然是满足条件的。
4. 避坑指南
-
为什么内存块那道题不能用 Kadane?
- Kadane 是单指针,只能一值向前走,它只在乎 和 有没有变大,不会在满足
target后,回过头去尝试缩短左边界。
- Kadane 是单指针,只能一值向前走,它只在乎 和 有没有变大,不会在满足
-
为什么可可塔不能用滑动窗口?
- 滑动窗口依赖 单调性,也就是说右移
right必然会让sum变大。如果数组里有负数,这个方法就没用了,窗口的伸缩会变得乱七八糟。
- 滑动窗口依赖 单调性,也就是说右移
5. 算法模型选择
看到下面的场景时,可以根据上文讲的 Kadane 和滑动窗口的特征选择合适的模型:
-
求连续一段的最大和?
- 优先考虑 Kadane。
-
求满足条件的连续最短或最长长度?
- 优先考虑滑动窗口。
-
数组里全是正数?
- 滑动窗口。
-
数组里正负数都有?
- 必须用 Kadane。