【算法】双指针与滑动窗口(一):框架总纲——三类问题、一个原理与判决书

【算法】双指针与滑动窗口(一):框架总纲------三类问题、一个原理与判决书

摘要

双指针/滑动窗口系列第一篇,只讲框架不讲题 。这个结构本身是一次纠错的结果:我先前按"一题一题做、批改里提炼要点"的方式学这个系列(LC3 写了五版、LC15 四版、LC76 一版过但被拷问),十来道题做完,某天被自己问倒------"双指针的核心原理到底是什么? "竟然答不上来。回溯系列我有"选择、递归、回退"六个字,DP 系列我有手推表,双指针......只有一堆散落的教训。这篇就是补课:一个统一原理 (被跳过的候选不可能是答案)、三大类地图 (滑动窗口/相向双指针/二分,各自的模板与旋钮)、一份判决书规范 (排除要有论证、幸存者要验明身份)、一套三问流程 (动笔前先答:哪个大类、哪个模板、判决书是什么)。后面三篇按类分篇讲题,题目是框架的例证------先有地图,再上路

前置阅读:动态规划第七篇(终章):单词拆分II------缓存结论的完全体,与两个系列的合龙。配套代码仓库(按题号分目录):https://github.com/a18792721831/studyleetCode

【算法】双指针与滑动窗口(一):框架总纲------三类问题、一个原理与判决书

  • 【算法】双指针与滑动窗口(一):框架总纲------三类问题、一个原理与判决书
    • 摘要
    • [1. 先认个错:我为什么学得这么狼狈](#1. 先认个错:我为什么学得这么狼狈)
    • [2. 一个统一原理](#2. 一个统一原理)
    • [3. 三门技术是一家](#3. 三门技术是一家)
    • [4. 三大类地图](#4. 三大类地图)
      • [4.1 大类一:滑动窗口(同向双指针)](#4.1 大类一:滑动窗口(同向双指针))
      • [4.2 大类二:相向双指针](#4.2 大类二:相向双指针)
      • [4.3 大类三:二分](#4.3 大类三:二分)
      • [4.4 大类之间的暗门](#4.4 大类之间的暗门)
    • [5. 三问流程(动笔前,答案写注释第一行)](#5. 三问流程(动笔前,答案写注释第一行))
    • [6. 我的翻车史挂回框架(自用检查表)](#6. 我的翻车史挂回框架(自用检查表))
    • [7. 框架生效的实证](#7. 框架生效的实证)
    • 总结
    • 参考资料

1. 先认个错:我为什么学得这么狼狈

先交代背景。这个系列开坑前,我刚走完回溯三篇 + DP 七篇,手推表一稿全过的状态。然后 LC3 无重复最长子串连写五版,LC15 三数之和四版,LC76 手推三轮------翻车密度比 DP 后期高出一个数量级

当时我归因为"范式切换的成本",这没错,但只说对了一半。真正的另一半在几个月后做二分系列时才暴露:某天我停下笔问自己"双指针的通用想法是什么",发现脑子里只有一题一题的碎片------LC3 的哨兵、LC15 的去重宿主、LC76 的三件套------没有一个能统一起来的骨架。对比之下,回溯我有万能模板(选择-递归-撤销三连),DP 我有五步法和手推表,遇到新题第一反应是"往哪个框架套"。双指针遇到新题,第一反应是"这题是不是又要从零发明"。

病根不在题,在教学顺序:回溯先给模板再做题,DP 由简入繁,而双指针我一直是一题一驱------地图从来没拿到过,只捡了一路的路标。 这篇就是补地图。如果你也在"题目做了不少、原理说不出"的状态,希望这篇能直接把骨架立起来。

2. 一个统一原理

所有双指针/滑动窗口题,不管长什么样,共享一句话:

把 O(n²) 的"枚举所有配对/所有区间"压成 O(n),压缩的合法性只有一种:证明"被跳过的候选,不可能是答案"。

逐字拆:

  • 候选:暴力解里你本该检查的每一对 (i, j) 或每个区间 l, r
  • 跳过:指针移动时被抛在身后的那一片
  • 证明:这是重点------跳过不是省事,是每一步都要携带"被排除者全体无罪"的论证

三个熟悉的化身:LC11 盛水的"谁矮动谁"(矮边的所有剩余配对 ≤ 已记录值)、LC3 滑窗的"left 只进不退"(被跳过的起点配任何终点都不超过已算的答案)、二分的"排除一半"(有序性证明另一半全体 ≠ target)。题型千变万化,压缩的合法性就这一种。

我把这份论证叫做判决书。它贯穿整个系列,规范有三条:

  1. 排除要有判决书:每次动指针,能说出"被排除的为什么不可能是答案"(LC11 的单调性证明是模板)
  2. 幸存者要验明身份 :循环退出时指针停在哪、那格是什么语义、是不是答案------三问缺一不可(LC35 的 return l 靠的是"l 左侧全 < target"这份汇总判决;LC34 忘了验 nums[l] != target 就输出垃圾)
  3. 买不到信息就退化 :论证不了排除,就只能缩小一格(LC81 的 l++------等值遮住断崖时,唯一安全的动作是扔掉一个已知非答案的格子)

3. 三门技术是一家

用"核心对象 + 核心动作"给三门算法对仗,双指针的位置立刻清晰:

核心对象 核心动作 一句话
回溯 路径(path) 选择、递归、回退 决策树上走路,走了要退
DP 状态表(dp 数组) 递推 格子由格子算出来
双指针 区间(窗口) 吃进、判定、吐出 两端夹逼一个区间,每步只动一个端点

回溯的合法性靠"回退还原",DP 靠"无后效性",双指针靠指针永不回头------而它的底气就是第 2 节的判决书。

三门还有一条演化关系(DP 终章写过前半):回溯是决策树上走路,DP 是把树折叠成表。补上后半句------

双指针是折叠的极限:当状态简单到一个区间就能描述,DP 的整张表退化成两个下标。

LC3 亲眼见过这个退化:dp[i] = i - left + 1,dp 数组蒸发,剩下 lr 两个变量。所以双指针不是新世界,是 DP 的特例------窗口足够简单,表就缩成了指针。这个视角的实用价值:遇到新题先问"这题的状态能不能用一个区间描述"------能,滑窗;状态更复杂,回 DP。

4. 三大类地图

4.1 大类一:滑动窗口(同向双指针)

识别信号:连续子串/子数组 + 窗口性质可增量维护(无重复/和/覆盖计数)。

复制代码
l = 0
for r := 0; r < n; r++ {
    进:window 加入 s[r],维护状态
    for 收缩条件 {              ← 旋钮一
        出:window 移除 s[l],维护状态
        l++
    }
    记录答案                    ← 旋钮二
}

模板固定,每题只调两个旋钮:

题型 收缩条件 记录时机 例题
最长 窗口不合法时收缩(恢复合法) 每轮扩张后 LC3 无重复最长子串
最短 窗口合法时收缩(收缩即产出,每格是候选) 收缩循环内 LC209 最短子数组 / LC76 最小覆盖

状态形态每题不同(一张计数表 / 一个和 / need-window-count 三件套),但纪律恒定:吃进动了什么账,吐出就销什么账------进出各维护一次,缺半个就是 bug。

4.2 大类二:相向双指针

识别信号排序后找配对(两数之和 / 三数之和 / 区间极值)。

复制代码
l, r = 两端
for l < r {
    用有序性做一次 O(1) 比较 → 排除一整排候选
    只动一个指针
}

比较的两种典型:和与 target 比(LC167 两数之和:和大 r--、和小 l++)、端点值互比(LC11 盛水:谁矮动谁)。一轮只动一个指针、动作互斥 ------我在这上面栽过著名的"平行 if 无互斥"四连犯(LC3 的三参数 max、LC15 的六 if 递归、LC33 的双 if 判定、LC81 的混搭收缩),症状全是同一轮指针走多条路。铁律:指针一轮一步。

4.3 大类三:二分

识别信号:有序 + 单点查询或找边界,O(log n) 硬要求。

两类模板,先分清再动笔:

复制代码
找点:  for l <= r { mid; 判定; mid±1 }        ← == 是终点,mid 出局
找边界:for l < r  { mid; 判定; 保 mid 收缩 }   ← == 是路标,mid 可能是答案

三条铁律:

  1. 循环条件与收缩方式配对l<=rmid±1l<rr=mid(保 mid)。拆开单换一个,就是"最后一格永不检查"或"l==r 死循环"
  2. 值域两端都要卡 :有序半是个闭区间,判定写 nums[l] <= target && target < nums[mid]------mid 端开着(判过 == 已出局),l/r 端闭着(还没判过,必须留在候选里)。只写 mid 一端是我犯过三次的病(见第 6 节)
  3. 判定锚点要无歧义 :LC153 找最小值锚点是 nums[r] 而非 nums[l](后者在未旋转数组上有歧义);LC33 判哪半有序锚点是 nums[l] vs nums[mid](配 <=,让 l==mid 的退化情况走对分支)

4.4 大类之间的暗门

  • LC15 三数之和 = 固定一个数 + 内层相向双指针(大类二嵌套)
  • LC33/153/81 旋转数组 = 二分 + 变化的判定设计(锚点选择 / 哪半有序 / 等值退化)
  • LC3 从 DP 走到滑窗 = 表退化成指针(第 3 节的演化关系)

5. 三问流程(动笔前,答案写注释第一行)

复制代码
1. 哪个大类?(滑窗 / 相向 / 二分)
2. 哪个模板、哪个旋钮?(两类模板 / 收缩条件 / 判定锚点)
3. 判决书是什么?(排除的合法性;循环退出时幸存者的身份)

实测过的价值:重做阶段用这套流程,上次的病三次都在纸面上现形、没进代码------LC33 重做的三问答到一半,"值域判断又只剩半边"当场被抓住(首次做时这个病是靠七个用例实测才暴露的)。三问的成本是两分钟,省的是一整轮提交-翻车-修复。

6. 我的翻车史挂回框架(自用检查表)

框架位置 一句话
LC3 断链套收缩、used 清空 大类一 收缩是挤掉一头,不是推倒重来
LC76 跳跃收缩、答案在表里消失 大类一 最短型收缩即产出,每格是候选,手推表不许一笔带过
LC15 六 if 递归、元素复用 大类二 一轮只动一个指针,互斥
LC33 找点模板套找边界题 大类三 先问 mid 可不可以出局
LC81 重发明丢值域两端、丢守卫 大类三 改代码用加法不用重写;旧版的守卫是血换的
LC34 值域半边(三犯) 大类三 排除式思维总漏端点------改用包含式:在不在值域里,两端都卡
平行 if 无互斥(四犯) 全类 指针一轮一步
l=mid 死循环 大类三 l 方向收缩只能配 mid+1(mid 向下取整偏向 l)

7. 框架生效的实证

同一批题,首次做 vs 带框架重做:

首次 重做
LC34 三版(线性扩张 → 死循环嵌套死代码 → 终版) 两版(核心一步到位,只丢一个守卫)
LC33 两版(七用例七错起步) 思考题抓病 + 一版全绿
LC153 两版(模板错配出恒假条件) 一版全绿

数据说话:坑没有白踩,但坑要挂到框架的钩子上才算资产------散着放,它们只是挫折;挂起来,它们是检查表的行。

总结

这篇没有一道题的完整题解,只有三样东西:一个原理 (被跳过的候选不可能是答案------压缩合法性的唯一来源)、一张地图 (三大类,各自的对象/动作/模板/旋钮/铁律)、一套纪律(三问流程 + 判决书三条规范)。

感触最深的一点写在这里:我此前把"会做题"当成目标,于是用题量堆熟练度;这个系列的教训是没有框架的题量只是翻车史的长短 。回溯六个字能统领十一道题,DP 一张表能统领六级------双指针的"六个字"我现在有了:吃进、判定、吐出;比较、排除、收缩。后面三篇按类讲题,每题都是这张地图上的一次点名。

下一篇:滑动窗口------吃进、判定、吐出,从 LC209 最短子数组开始。

参考资料


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

相关推荐
shehuiyuelaiyuehao1 小时前
算法42,模拟算法,模拟z字形变换
算法
朝朝辞暮i1 小时前
C++第一课
开发语言·c++·算法
橘子汽水1681 小时前
Leetcode 208,207实现Trie前缀树,课程表
java·数据结构·算法·leetcode
hanlin031 小时前
刷题笔记:力扣第169题-多数元素
笔记·算法·leetcode
hfywmsj1 小时前
广州餐饮铺位招租数据建模:基于多维评估体系的选址算法设计与实现
人工智能·算法·广州餐饮铺位招租
wabs6661 小时前
关于二叉树【力扣116.填充每个节点的下一个右侧节点指针的思考】
数据结构·c++·算法·leetcode·二叉树·层序遍历
Nil2082 小时前
leetcode 74搜索二维矩阵
算法·leetcode·矩阵
新知图书2 小时前
第 14 章 后训练算法GRPO详解与实战
人工智能·算法
shirsl4 小时前
算法 Day1-数组 / 哈希 + 双指针
python·算法·哈希算法