【算法对比】连续子区间的两种控制逻辑:Kadane和滑动窗口

0 阅读5分钟

0. 前言

在做数组相关的算法题时,连续子区间 是出现频率最高,也是花样最多的一个考点。

本文将通过两道算法题,深度对比处理连续子区间的两大方法:针对最大和的 Kadane 算法针对最小区间长度的滑动窗口

1. 问题描述

1.1 岁晚可可塔

这道题其实就是求最大连续总和,数组元素有正有负。

1. 问题描述.png

2. 输入输出.png

1.2 内存块的最短分配

这道题其实就是求符合要求的最短连续长度。

现在有一排连续的内存块,每个内存块的大小不一,由一个正整数数组 nums 表示。现在需要分配一个 连续 的内存区域,使得该区域内所有内存块的大小总和 大于等于 给定的目标值 target

请找出满足条件的 最短连续子数组 的长度。如果不存在符合条件的子数组,返回 0。

输入:

  • 第一行:一个整数 target,目标内存大小。
  • 第二行:一个整数 n,代表内存块的总数量。
  • 第三行:n 个由空格分隔的正整数,每个正整数代表对应的每个内存块的大小。

输出:

  • 一个整数,代表满足条件的 最短连续子数组的长度

2. 算法逻辑对比

我用一个表格总结了一下 Kadane 算法和滑动窗口的算法逻辑,方便大家根据合适的场景进行选择:

3. 对比表格.png

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];//每遍历到一个元素,就把它加到sumwhile(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 后,回过头去尝试缩短左边界。
  • 为什么可可塔不能用滑动窗口?

    • 滑动窗口依赖 单调性,也就是说右移 right 必然会让 sum 变大。如果数组里有负数,这个方法就没用了,窗口的伸缩会变得乱七八糟。

5. 算法模型选择

看到下面的场景时,可以根据上文讲的 Kadane 和滑动窗口的特征选择合适的模型:

  • 求连续一段的最大和?

    • 优先考虑 Kadane。
  • 求满足条件的连续最短或最长长度?

    • 优先考虑滑动窗口。
  • 数组里全是正数?

    • 滑动窗口。
  • 数组里正负数都有?

    • 必须用 Kadane。