ACM 学习篇 04:双指针与滑动窗口

双指针解决什么问题

如果一道题让你枚举所有区间,最直接的写法是两层循环:左端点一层,右端点一层。这样通常是 O(n^2)

双指针想做的事情,是让左端点和右端点都只往前走。每个元素最多被左指针经过一次、被右指针经过一次,整体就可能降到 O(n)

所以双指针的重点不是“写两个变量”,而是确认这两个变量能不能单调移动。

相向双指针

相向双指针通常用在有序数组里。一个指针从左边走,一个指针从右边走。

比如经典的两数之和:数组已经有序,找是否存在 a[l] + a[r] == target

int l = 0, r = n - 1;
while (l < r) {
    int sum = a[l] + a[r];
    if (sum == target) {
        cout << "YES\n";
        break;
    } else if (sum < target) {
        l++;
    } else {
        r--;
    }
}

为什么可以这样走?因为数组有序。当前和太小,左指针右移才可能变大;当前和太大,右指针左移才可能变小。这就是单调性。

同向双指针

同向双指针更像是在维护一个区间。右指针不断扩张,左指针在条件不满足时向右收缩。

比如所有元素为正数,找和至少为 S 的最短连续子数组:

int ans = n + 1;
long long sum = 0;
int l = 0;
for (int r = 0; r < n; r++) {
    sum += a[r];
    while (sum >= S) {
        ans = min(ans, r - l + 1);
        sum -= a[l];
        l++;
    }
}

这里能成立的关键是数组元素为正。右指针右移,窗口和只会变大;左指针右移,窗口和只会变小。如果有负数,这个单调性就不一定存在。

滑动窗口

滑动窗口可以看成同向双指针的一种常见形式:维护一个连续窗口,让窗口满足某个条件。

比如最长不含重复字符的子串,可以用哈希表记录窗口里每个字符出现次数。

int ans = 0;
vector<int> cnt(256, 0);
int l = 0;
for (int r = 0; r < (int)s.size(); r++) {
    cnt[s[r]]++;
    while (cnt[s[r]] > 1) {
        cnt[s[l]]--;
        l++;
    }
    ans = max(ans, r - l + 1);
}

这段代码的节奏很固定:右边加入一个元素,窗口不合法就从左边删,合法后更新答案。

固定长度窗口

有些题窗口长度固定,比如“长度为 k 的子数组最大和”。这种题甚至不需要 while,只要保持窗口大小就行。

long long sum = 0, ans = LLONG_MIN;
for (int r = 0; r < n; r++) {
    sum += a[r];
    if (r >= k) sum -= a[r - k];
    if (r >= k - 1) ans = max(ans, sum);
}

固定窗口的关键是:每次右边进来一个,左边最多出去一个。它更像一个队列。

分组双指针

还有一种很常见的写法叫分组双指针:把连续相同、连续满足某种性质的一段一次性处理掉。

int i = 0;
while (i < n) {
    int j = i;
    while (j < n && a[j] == a[i]) j++;
    // [i, j) 是一段相同元素
    int len = j - i;
    i = j;
}

这类写法在去重、统计连续段、压缩字符串、处理相同颜色块时很常见。

什么时候不能用

双指针最怕的是你以为它能动,但其实没有单调性。

  • 数组里有负数时,窗口和不一定随右指针增加而增加;
  • 条件不是连续区间性质时,删掉左端点可能影响很复杂;
  • 排序会破坏原始位置关系时,不能随便先排序;
  • 左右指针移动方向不明确时,可能还是需要别的数据结构或 DP。

写双指针前,最好先问一句:为什么这个指针一旦右移,就不需要再回头?能回答清楚,才是真的能用。

这一篇先记住什么

  • 双指针的核心是单调移动,不是两个变量。
  • 相向双指针常配合排序,同向双指针常维护连续窗口。
  • 滑动窗口的基本节奏是:右边加入,左边收缩,合法后更新答案。
  • 只要指针会回头,复杂度就可能不再是线性的。

下一篇进入栈、队列和单调结构。它们会把“最近一个更大值”“窗口最大值”这类问题处理得很漂亮。