滑动窗口:先定「窗口里维护什么」,再写循环

窗口能成立的前提是「窗口是否合法」随左右边界单调。先写下维护的状态与收缩条件,双指针的两层循环自然会摊还成 O(n)。

滑动窗口的难点从来不是双指针,而是窗口里维护什么状态。想清楚那个状态,两层循环会自然收敛;想不清楚,写出来的版本会把同一个字符反复数。

模板:右扩张,左收缩

以「最长无重复子串」为例:窗口是 [left, right],维护状态是「窗口内每个字符最后出现的下标」。右边界每前进一格,如果这个字符上次出现在窗口内,左边界就跳到它的下一位。

function longestUnique(s) {
  const seen = new Map();
  let left = 0;
  let best = 0;
  for (let right = 0; right < s.length; right += 1) {
    const ch = s[right];
    const last = seen.get(ch);
    if (last !== undefined && last >= left) left = last + 1;
    seen.set(ch, right);
    best = Math.max(best, right - left + 1);
  }
  return best;
}

注意 last >= left 这个判断:字符可能在窗口外出现过(更早),那就与当前窗口无关,不该动左边界。初学者最常犯的错就是只用 seen.has(ch)。

为什么是 O(n)

right 和 left 都只向右移动,从不回头:right 走 n 步,left 总共也走不超过 n 步。所以虽然写了两层结构,总操作数 ≤ 2n。前提是收缩条件在左边界右移时不会反向。

什么条件下不该用

如果「窗口是否合法」不随窗口变大单调变化(变大了可能合法、也可能不合法,且没有规律),那左边界右移就无法恢复合法性,双指针失效。这时要么换状态设计(把「不合法」定义成单调的量),要么老实用前缀和 / 单调队列 / 二分。

同一模板的三个变体

问题 维护的状态 收缩条件
最长无重复子串 字符最后下标 出现重复
最小覆盖子串 每种字符还差几个 已覆盖全部字符
定长子数组最大和 窗口和 长度超过 k

「最小覆盖」那类要在收缩的循环里更新答案,而不是在扩张时 —— 因为要的是最短的合法窗口,收缩的每一刻都可能是答案。

窗口成立与否要单调:右边界扩张会让它从合法变不合法(或反过来),左边界收缩能把它修回来。

← 返回文章列表

评论

…