基于本地 01–13 专题 · Python 3
AUTUMN RECRUITMENT · ALGORITHM PLAYBOOK

秋招算法:
从题意到模板,从代码到表达。

结合本地 01–13 学习手册,把零散题解整理成一套可迁移的解题流程。每个专题都有识别信号、状态约定、适用条件、Python 3 模板和面试讲法。

13专题43组模板105道去重原题离线可读可打印

根据现有手册归纳;“优先练习”是为本题集制定的复习顺序,不代表企业出题频率统计。原手册共 110 个题目条目,含跨专题与 DFS/BFS 重复。模板按题型分组,不重复抄写全部原题答案。

01 / 一道题的六步解题流程

复述问题,确认约束

输入输出是什么?连续还是子序列?求数量、最值还是所有方案?能否修改输入?有序吗?有负数、重复值、环吗?

用规模筛掉不可行方案

写暴力基线。n≈10⁵ 通常先寻 O(n) 或 O(n log n);n≈10³ 可评估 O(n²);n≤20 可考虑子集搜索。它们只是量级提示,仍要看时限、语言、常数和测试组总量。

找到重复工作或结构性质

重复查历史→哈希;重复算区间→前缀;候选有单调性→二分;连续区间可单向收缩→窗口;重复子问题→DP。

先写状态和不变量

用一句完整的话定义每个变量、函数返回值、区间端点。写出基例、转移或移动规则,并说明为什么不漏解。

实现最小正确版本

先数组后压空间;先递归契约后显式栈;共享状态进入前保存、返回时恢复。算法、解析输入、格式化输出分开。

手推、测边界、讲复杂度

正常例子后,再测最小输入、无解、全相等、负数/零、极端结构。数每个元素或状态被处理几次,同时计入栈、切片和答案输出。

卡住时的回退法:先枚举一个答案或最后一步,写出正确暴力;标出重复查询或重复状态,再考虑压缩。先写“为什么可以这样做”,再挑模板。

02 / 读完题,如何选第一条路?

问题信号先尝试必须验证
查两数、频次、重复、分组哈希表键保存什么?查存在、数量、位置,还是对象映射?
数组原地过滤、两端配对、有序归并双指针排序是否合法?移动是否能排除候选?
连续区间的最长/最短/定长窗口 / 前缀和窗口能否单向推进?含负数的精确和常转前缀哈希。
有序边界、最小可行答案二分判定单调?候选域与返回哨兵是什么?
最近更大/更小、固定窗口极值单调栈/队列方向、严格性、过期条件、答案何时结算?
节点改向、中点、倒数、环链表指针模型对象身份?dummy?后继是否已保存?
树的路径、子树汇总或逐层访问DFS/BFS/树形 DP信息往下传还是往上返?答案能否左右分叉?
网格连通、最少步数、同时传播DFS / BFS / 多源 BFS边权相同?visited 是永久还是路径状态?
输出全部排列、组合、切分回溯start/used/位置选哪个?怎么避免重复?
只求可行、数量或最优且子问题重复记忆化 / DP状态是否足够?最后一步如何分类?
局部选择似乎总最优尝试证明贪心交换、支配或上界可达能否证明?否则回到 DP。

分类可以组合:560 是前缀和+哈希;239 是窗口+单调队列;437 是树 DFS+前缀和+回溯;300 的进阶解是贪心状态+二分。

03 / 最容易套错的 10 组条件

易混场景正确分界反例 / 自检
窗口 vs 前缀哈希209 全正数最短达标可缩;560 任意整数精确计数查历史。[1,−1,1],k=1:答案 3,和的变化不单调。
最长 vs 最短窗口最长在恢复合法后记录;最短在合法状态下,移出前记录。76 的 t="AA" 需两个 A,集合不能表达频次。
子数组 vs 子序列子数组必须连续;子序列保序但可跳选。"abc" 在 "axbyc" 中是子序列,不是子串。
组合 vs 排列组合靠递增 start 避免顺序重复;排列靠 used 防止重复用位置。[1,2] 与 [2,1] 是不同排列、同一组合。
visited vs path连通块永久标记;路径搜索返回恢复;树路径前缀频次也恢复。79 中同格能被不同候选路径再次使用。
返回值 vs 全局答案124 返回单支,当前节点的全局候选可连双支。根 1,左右 2、3:全局为 6,向上只能返回 4。
前缀最优 vs 结尾最优198 可以不选当前;53 必须以当前结尾再求全局最大。[−2,−1] 的非空最大和为 −1,不能初始化为 0。
首个 ≥ vs 首个 >lower_bound 与 upper_bound 只差等号,返回的是插入边界。[2,2,2] 查 2:左边界 0,右开边界 3。
一次交易 vs 不限次121 保存最低买价;122 才可累加全部正差。[1,3,2,4]:一次 3,不限次 4。
层宽 vs 面积 vs 距离695 累计访问数量;994 统计层数;单层宽度不是面积。长蛇岛屿层宽很小,但面积可很大。

04 / 代码约定与最小接口

下面均为 Python 3 核心函数,按每组标出的前提调用。可直接在本地运行;提交 LeetCode 时按题目接口改为 class Solution 的方法并加入 self。笔试 ACM 模式另外处理输入输出,不把所有模板一次粘贴进提交文件。

链表与树模板共用以下节点定义;平台已提供类时使用平台定义。多个代码组互不依赖,除链表/树共用节点类外,组内包含所需导入。哈希复杂度按期望/均摊 O(1) 操作分析;整数按常见单位代价模型分析,超大整数的位成本另计。

展开公共节点定义(本地测试用)
Python 3 · 核心模板
class ListNode:
    def __init__(self, val=0, next=None):
        self.val, self.next = val, next

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right

class RandomNode:
    def __init__(self, val=0, next=None, random=None):
        self.val, self.next, self.random = val, next, random

05 / 13 专题模板库

先看识别信号与不变量,再阅读代码。下方搜索只筛选模板库;流程、面试与自测始终保留。每章原题目录可跳回原手册对应位置。

已开启速记模式:隐藏代码,只看判断条件、模板说明和复杂度;点击顶部“显示完整代码”恢复。

PATTERN 01

01 / 双指针

识别信号
有序配对、原地删除、两端对称、两个序列协作。
推导顺序
先确认是否有序、能否排序、是否需要保留下标,再决定相向、读写或双序列指针。每移动一次,都要说出被排除的候选为何不可能更优。
核心不变量
读写模型:nums[:write] 是已处理部分中应保留的元素;相向模型:答案仍在尚未排除的候选中。
易错边界
排序会改变原下标;三数之和必须三层去重;荷兰旗交换右端后不能直接前进;接雨水的边界高度不是当前单柱高度。
面试讲法
我用两个边界维护候选集合;利用有序性或支配关系排除一侧,每次至少推进一个指针。

原题变式速记:344/125:两端交换或跳过无效字符;1768/392:各自推进两个序列;88:从后向前写,防止覆盖;27/26/283:压缩有效前缀;11:移动短板;15:排序固定一数;75:三路划分;42:左右最高边界;28:朴素匹配或 KMP。

01.01 相向搜索 + 排序去重

三数之和;输入值可排序,返回值组合而非原下标。

复杂度:O(n²) 时间;排序与结果空间另计,Python 排序最坏 O(n) 辅助。

Python 3 · 核心模板
def three_sum(nums):
    a = sorted(nums)
    ans = []
    for i in range(len(a) - 2):
        if i > 0 and a[i] == a[i - 1]:
            continue
        if a[i] > 0:
            break
        left, right = i + 1, len(a) - 1
        while left < right:
            total = a[i] + a[left] + a[right]
            if total < 0:
                left += 1
            elif total > 0:
                right -= 1
            else:
                ans.append([a[i], a[left], a[right]])
                left += 1
                right -= 1
                while left < right and a[left] == a[left - 1]:
                    left += 1
                while left < right and a[right] == a[right + 1]:
                    right -= 1
    return ans

01.02 同向读写 + 双序列保序

原地移除目标值;判断 s 是否为 t 的子序列。

复杂度:均 O(n) 级扫描、O(1) 辅助;子序列为 O(|t|)。

Python 3 · 核心模板
def remove_value(nums, target):
    write = 0
    for read in range(len(nums)):
        if nums[read] != target:
            nums[write] = nums[read]
            write += 1
    return write  # 只有 nums[:write] 有效

def is_subsequence(s, t):
    i = 0
    for ch in t:
        if i < len(s) and s[i] == ch:
            i += 1
    return i == len(s)

01.03 双边界结算 + 三路划分

42 接雨水;75 的输入只包含 0、1、2。

复杂度:各 O(n) 时间、O(1) 辅助。

Python 3 · 核心模板
def trap_water(height):
    left, right = 0, len(height) - 1
    left_max = right_max = ans = 0
    while left <= right:
        if left_max <= right_max:
            left_max = max(left_max, height[left])
            ans += left_max - height[left]
            left += 1
        else:
            right_max = max(right_max, height[right])
            ans += right_max - height[right]
            right -= 1
    return ans

def sort_colors(nums):
    low = cur = 0
    high = len(nums) - 1
    # [0,low) 为0;[low,cur) 为1;[cur,high] 未知
    while cur <= high:
        if nums[cur] == 0:
            nums[low], nums[cur] = nums[cur], nums[low]
            low += 1
            cur += 1
        elif nums[cur] == 2:
            nums[cur], nums[high] = nums[high], nums[cur]
            high -= 1  # 换来的数还未检查
        else:
            cur += 1

01.04 KMP:匹配失败后复用前后缀

28 的进阶匹配;大量重复字符时避免回退文本指针。

复杂度:O(n+m) 时间、O(m) 辅助。

Python 3 · 核心模板
def kmp_find(text, pattern):
    if not pattern:
        return 0
    pi = [0] * len(pattern)
    j = 0
    for i in range(1, len(pattern)):
        while j and pattern[i] != pattern[j]:
            j = pi[j - 1]
        if pattern[i] == pattern[j]:
            j += 1
        pi[i] = j
    j = 0
    for i, ch in enumerate(text):
        while j and ch != pattern[j]:
            j = pi[j - 1]
        if ch == pattern[j]:
            j += 1
        if j == len(pattern):
            return i - j + 1
    return -1
回到原手册 · 双指针的题目目录(13 个条目)
PATTERN 02

02 / 哈希表

识别信号
快速判断存在、查补数、统计频次、按特征分组、去重。
推导顺序
先决定键和值分别保存什么:存在用 set,数量用 Counter,下标用 dict;再检查题目是否要求 O(1) 辅助空间。
核心不变量
单遍查补数时,表中仅有当前位置之前的元素;分组键必须对同类对象一致、对异类对象可区分。
易错边界
两数之和先查再存;重复值不能覆盖成同一节点或同一下标;136 异或要求其余数各出现两次;169 投票依赖多数元素保证,否则需验证。
面试讲法
暴力重复查询历史元素,我把历史信息压成键到信息的映射,用空间换取均摊常数时间查询。

原题变式速记:1:补数映射;136:异或抵消;169:Boyer–Moore 投票;49:排序字符串或频次数组作键;128:只从连续段起点展开;41:把值 x 放到下标 x−1。

02.01 查补数 / 计数 / 分组

存在、下标、计数是不同的信息需求。

复杂度:two_sum 期望 O(n)/O(n);分组 O(Σ L log L),输出字符与键空间另计。

Python 3 · 核心模板
from collections import Counter, defaultdict

def two_sum(nums, target):
    seen = {}
    for i, x in enumerate(nums):
        if target - x in seen:
            return [seen[target - x], i]
        seen[x] = i
    return []

def group_anagrams(words):
    groups = defaultdict(list)
    for word in words:
        groups[''.join(sorted(word))].append(word)
    return list(groups.values())

02.02 集合只从起点展开 + 原地索引映射

128 连续序列;41 正整数缺失值,允许修改数组。

复杂度:前者期望 O(n) 时间/O(n) 空间;后者 O(n) 时间/O(1) 辅助。

Python 3 · 核心模板
def longest_consecutive(nums):
    values = set(nums)
    best = 0
    for x in values:
        if x - 1 not in values:
            end = x
            while end in values:
                end += 1
            best = max(best, end - x)
    return best

def first_missing_positive(nums):
    n = len(nums)
    for i in range(n):
        while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
            j = nums[i] - 1
            nums[i], nums[j] = nums[j], nums[i]
    for i, x in enumerate(nums):
        if x != i + 1:
            return i + 1
    return n + 1

02.03 特殊约束:抵消与投票

只在出现次数约束成立时使用。

复杂度:各 O(n) 时间、O(1) 辅助。

Python 3 · 核心模板
def single_number(nums):  # 其余每个值恰好出现两次
    value = 0
    for x in nums:
        value ^= x
    return value

def majority_element(nums):  # 保证非空且存在 > n/2 的元素
    candidate = None
    votes = 0
    for x in nums:
        if votes == 0:
            candidate = x
        votes += 1 if x == candidate else -1
    return candidate
回到原手册 · 哈希表的题目目录(6 个条目)
PATTERN 03

03 / 滑动窗口

识别信号
连续子串/子数组的最长、最短、定长统计;边界能单向推进。
推导顺序
先区分最长合法、最短覆盖、固定长度。再证明左端移走的元素以后不必回来,不能仅因为出现“连续”就选窗口。
核心不变量
状态精确对应 [left,right];最长:非法时缩,合法后记录;最短:合法时先记录,再缩。
易错边界
用 while 而非一次 if;更新长度是 right−left+1;有负数的和不具普通窗口所需单调性;最小覆盖按需求频次而非只看种类存在。
面试讲法
右端扩张引入信息,左端只向右恢复或逼近约束。两端各走至多 n 步,因此在常数状态更新下是 O(n)。

原题变式速记:3:最长无重复;438:长度为 |p| 且频次一致;76:覆盖需求后不断收缩;239:窗口状态换成单调队列,见第 7 章。

03.01 最长合法窗口

3 无重复字符;用最后位置跳过非法前缀。

复杂度:期望 O(n) 时间;O(min(n,Σ)) 空间。

Python 3 · 核心模板
def longest_unique(s):
    last = {}
    left = best = 0
    for right, ch in enumerate(s):
        left = max(left, last.get(ch, -1) + 1)
        last[ch] = right
        best = max(best, right - left + 1)
    return best

03.02 最短覆盖窗口

76 允许重复需求字符;空需求返回空串。

复杂度:期望 O(|s|+|t|) 时间;O(Σ) 空间,最后切片计入输出。

Python 3 · 核心模板
from collections import Counter, defaultdict

def min_window(s, t):
    if not t:
        return ''
    need = Counter(t)
    have = defaultdict(int)
    formed = left = 0
    best_len, best_left = float('inf'), 0
    for right, ch in enumerate(s):
        if ch in need:
            have[ch] += 1
            if have[ch] == need[ch]:
                formed += 1
        while formed == len(need):
            if right - left + 1 < best_len:
                best_len, best_left = right - left + 1, left
            old = s[left]
            if old in need:
                if have[old] == need[old]:
                    formed -= 1
                have[old] -= 1
            left += 1
    return '' if best_len == float('inf') else s[best_left:best_left+best_len]

03.03 固定长度频次匹配

438:示例约定 s 与 p 仅包含小写英文字母,p 非空。

复杂度:O(26n+|p|)=O(n+|p|),辅助 O(26),结果另计。

Python 3 · 核心模板
def find_anagrams(s, p):
    if not p or len(p) > len(s):
        return []
    need, window = [0] * 26, [0] * 26
    for ch in p:
        need[ord(ch) - ord('a')] += 1
    ans, k = [], len(p)
    for i, ch in enumerate(s):
        window[ord(ch) - ord('a')] += 1
        if i >= k:
            window[ord(s[i-k]) - ord('a')] -= 1
        if i >= k - 1 and window == need:
            ans.append(i - k + 1)
    return ans
回到原手册 · 滑动窗口的题目目录(4 个条目)
PATTERN 04

04 / 二分查找

识别信号
有序数组边界;判定结果随候选值单调;旋转数组局部有序。
推导顺序
先把问题改成寻找第一个 True。写出搜索域、判定方向和无解返回值,再固定一套区间约定。
核心不变量
半开区间 [left,right):left 左侧全 False,right 及右侧全 True;循环结束 left=right 是分界。
易错边界
找精确值需二次验证;lower_bound 可返回 n;不能混用闭区间的 right=mid−1;240 仅行列有序,不能像 74 一样整体拉平;旋转模板默认元素互异。
面试讲法
我对候选值构造单调判定,每步排除不可能含首个可行答案的一半区间;明确终止后边界的语义。

原题变式速记:704/35/34:等值与左右边界;69:整数平方根可用首个平方超界值−1;74:全局行序可映射一维;240:右上阶梯;153/33:识别旋转有序半边;4:在短数组二分分割线。

04.01 统一边界 / 二分答案

first_true 的区间为整数 [lo,hi),找不到返回原 hi;check 必须 False→True。

复杂度:O(log N × check成本),O(1) 辅助。

Python 3 · 核心模板
def first_true(lo, hi, check):
    while lo < hi:
        mid = (lo + hi) // 2
        if check(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

def lower_bound(nums, target):
    return first_true(0, len(nums), lambda i: nums[i] >= target)

def upper_bound(nums, target):
    return first_true(0, len(nums), lambda i: nums[i] > target)

def equal_range(nums, target):
    left, right = lower_bound(nums, target), upper_bound(nums, target)
    return [-1, -1] if left == right else [left, right - 1]

def integer_sqrt(x):  # x >= 0
    return first_true(0, x + 1, lambda v: v * v > x) - 1

04.02 旋转数组:保留有序半边

33 搜索;153 求最小值;输入非空且原升序数组元素互异。

复杂度:O(log n) 时间、O(1) 辅助。

Python 3 · 核心模板
def rotated_search(nums, target):
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] == target:
            return mid
        if nums[left] <= nums[mid]:
            if nums[left] <= target < nums[mid]:
                right = mid - 1
            else:
                left = mid + 1
        else:
            if nums[mid] < target <= nums[right]:
                left = mid + 1
            else:
                right = mid - 1
    return -1

def rotated_min(nums):
    left, right = 0, len(nums) - 1
    while left < right:
        mid = (left + right) // 2
        if nums[mid] > nums[right]:
            left = mid + 1
        else:
            right = mid
    return nums[left]

04.03 双数组中位数:二分分割线

4:两个升序数组,总长度至少为 1。

复杂度:O(log(min(m,n)+1)) 时间、O(1) 辅助。

Python 3 · 核心模板
def median_two_sorted(a, b):
    if len(a) > len(b):
        a, b = b, a
    m, n = len(a), len(b)
    half = (m + n + 1) // 2
    lo, hi = 0, m
    while lo <= hi:
        i = (lo + hi) // 2
        j = half - i
        al = a[i-1] if i else -float('inf')
        ar = a[i] if i < m else float('inf')
        bl = b[j-1] if j else -float('inf')
        br = b[j] if j < n else float('inf')
        if al <= br and bl <= ar:
            if (m + n) % 2:
                return max(al, bl)
            return (max(al, bl) + min(ar, br)) / 2
        if al > br:
            hi = i - 1
        else:
            lo = i + 1
    raise ValueError('输入必须有序且总长度非零')
回到原手册 · 二分查找的题目目录(9 个条目)
PATTERN 05

05 / 前缀和

识别信号
大量静态区间和;连续子数组和恰为 k;排除自身的聚合。
推导顺序
统一前缀下标是边界。区间和为 k 推出 P[left]=P[right]−k,再根据求数量或长度决定保存频次或最早位置。
核心不变量
P[i] 是前 i 项之和;查询历史前缀时,表中只有严格早于当前边界的前缀。
易错边界
频次表从 {0:1} 开始,先查再存;不能用 set 代替频次;带负数的 560 不能用普通和窗口;238 不用除法才能处理零。
面试讲法
把枚举区间的二维问题改写成前缀之间的关系,再用哈希表聚合所有合法左边界。

原题变式速记:209:全正数,最短达标窗口优先,也可前缀+二分;560:前缀差+频次;238:不含自身的左右积。

05.01 静态区间和 + 历史前缀计数

区间统一 [left,right);560 允许正数、零、负数。

复杂度:构建 O(n)/O(n),单次区间和 O(1);560 期望 O(n)/O(n)。

Python 3 · 核心模板
def build_prefix(nums):
    prefix = [0]
    for x in nums:
        prefix.append(prefix[-1] + x)
    return prefix

def subarray_sum_k(nums, k):
    freq = {0: 1}
    total = ans = 0
    for x in nums:
        total += x
        ans += freq.get(total - k, 0)  # 先查询
        freq[total] = freq.get(total, 0) + 1  # 再登记
    return ans

05.02 正数最短窗口 + 前后缀乘积

209 约定 target>0 且 nums 全正;238 允许零。

复杂度:均 O(n) 时间;窗口 O(1) 辅助,乘积除输出外 O(1)。

Python 3 · 核心模板
def min_positive_length(nums, target):
    left = total = 0
    best = len(nums) + 1
    for right, x in enumerate(nums):
        total += x
        while total >= target:
            best = min(best, right - left + 1)
            total -= nums[left]
            left += 1
    return 0 if best > len(nums) else best

def product_except_self(nums):
    ans = [1] * len(nums)
    product = 1
    for i, x in enumerate(nums):
        ans[i] = product
        product *= x
    product = 1
    for i in range(len(nums)-1, -1, -1):
        ans[i] *= product
        product *= nums[i]
    return ans
回到原手册 · 前缀和的题目目录(3 个条目)
PATTERN 06

06 / 贪心算法

识别信号
局部选择能支配其余选择;可达范围、独立收益、约束闭包。
推导顺序
先写局部决策,再用交换论证、上界可达或覆盖支配证明;发现选择之间有额外耦合时考虑 DP。
核心不变量
覆盖边界 far 表示从已证可达位置出发能覆盖的最远下标;只有可达位置才可扩展它。
易错边界
不能看到最优就贪心;121 一次交易和 122 不限交易不同;手续费/冷冻期会破坏正差独立性;45 不是每次跳到最远落点。
面试讲法
这个局部决策不损失最优性,因为它保留至少同样大的后续可选集合,或能达到所有方案的收益上界。

原题变式速记:121:前缀最低价;122:正差收益;55:单层覆盖;45:分层覆盖;763:段内所有字符最后位置的闭包。

06.01 前缀最优与独立正收益

121 最多一笔;122 无手续费、无冷冻期、不限笔数。

复杂度:O(n) 时间、O(1) 辅助。

Python 3 · 核心模板
def stock_once(prices):
    low, best = float('inf'), 0
    for price in prices:
        best = max(best, price - low)
        low = min(low, price)
    return best

def stock_unlimited(prices):
    return sum(max(0, prices[i] - prices[i-1])
               for i in range(1, len(prices)))

06.02 可达覆盖 / 最短分层覆盖

55 与 45:非负最大跳长,非空数组;min_jumps 额外对不可达返回 −1。

复杂度:均 O(n) 时间、O(1) 辅助。

Python 3 · 核心模板
def can_jump(nums):
    far = 0
    for i, step in enumerate(nums):
        if i > far:
            return False
        far = max(far, i + step)
    return True

def min_jumps(nums):
    end = far = jumps = 0
    for i in range(len(nums)-1):
        if i > far:
            return -1
        far = max(far, i + nums[i])
        if i == end:
            if far == end:
                return -1
            jumps += 1
            end = far
    return jumps

06.03 约束闭包:最早合法切分

763:一个字符的全部出现必须在同一段。

复杂度:O(n) 时间;O(Σ) 辅助,返回段长另计。

Python 3 · 核心模板
def partition_labels(s):
    last = {ch: i for i, ch in enumerate(s)}
    start = end = 0
    ans = []
    for i, ch in enumerate(s):
        end = max(end, last[ch])
        if i == end:
            ans.append(end - start + 1)
            start = i + 1
    return ans
回到原手册 · 贪心算法的题目目录(5 个条目)
PATTERN 07

07 / 栈和队列

识别信号
括号嵌套、撤销最近状态、下一个更大/更小、窗口最值。
推导顺序
普通栈保留未完成上下文;单调栈保留尚未结算的下标;单调队列额外从头部移除过期下标。先写比较关系再选等号。
核心不变量
单调栈每个下标只入栈一次、出栈至多一次;窗口队列下标递增,值递减,头部总在窗口内。
易错边界
求距离要存下标;相等弹不弹由严格性决定;柱形宽度是 i−stack[-1]−1;list.pop(0) 非 O(1),队列用 deque。
面试讲法
新元素到达时能一次结算若干旧元素,已弹出的候选被支配,不会再次入栈,所以嵌套循环总计线性。

原题变式速记:20:括号匹配;155:每层保存前缀最小;394:外层串和倍数入栈;32:非法边界哨兵−1;739:右侧首个更大;84:两侧更小围成面积;239:过期+支配淘汰。

07.01 普通栈 / 增强栈 / 嵌套解析

20、155、394;decode 约定输入语法合法,数字是重复次数。

复杂度:匹配 O(n)/O(n);MinStack 每次 O(1);解码时间受展开结果与嵌套拼接影响,不能只按输入长度算。

Python 3 · 核心模板
def valid_brackets(s):
    pairs = {')': '(', ']': '[', '}': '{'}
    stack = []
    for ch in s:
        if ch in pairs.values():
            stack.append(ch)
        elif ch in pairs:
            if not stack or stack.pop() != pairs[ch]:
                return False
        else:
            return False
    return not stack

class MinStack:
    def __init__(self):
        self.stack = []
    def push(self, x):
        low = min(x, self.stack[-1][1]) if self.stack else x
        self.stack.append((x, low))
    def pop(self):
        return self.stack.pop()[0]
    def top(self):
        return self.stack[-1][0]
    def getMin(self):
        return self.stack[-1][1]

def decode_string(s):
    stack, cur, number = [], '', 0
    for ch in s:
        if ch.isdigit():
            number = number * 10 + int(ch)
        elif ch == '[':
            stack.append((cur, number))
            cur, number = '', 0
        elif ch == ']':
            prefix, times = stack.pop()
            cur = prefix + cur * times
        else:
            cur += ch
    return cur

07.02 单调栈:右侧第一个严格更大

739 返回等待距离;找不到为 0。

复杂度:O(n) 时间、O(n) 辅助与输出。

Python 3 · 核心模板
def daily_temperatures(temperatures):
    ans = [0] * len(temperatures)
    stack = []
    for i, value in enumerate(temperatures):
        while stack and temperatures[stack[-1]] < value:
            j = stack.pop()
            ans[j] = i - j
        stack.append(i)
    return ans

07.03 边界结算:柱形面积 / 有效括号

84 柱高非负;32 输入只有左右圆括号。

复杂度:各 O(n) 时间、O(n) 辅助。

Python 3 · 核心模板
def largest_rectangle(heights):
    a = heights + [0]
    stack = [-1]
    best = 0
    for i, value in enumerate(a):
        while stack[-1] != -1 and a[stack[-1]] > value:
            h = a[stack.pop()]
            best = max(best, h * (i - stack[-1] - 1))
        stack.append(i)
    return best

def longest_valid_parentheses(s):
    stack, best = [-1], 0
    for i, ch in enumerate(s):
        if ch == '(':
            stack.append(i)
        else:
            stack.pop()
            if not stack:
                stack.append(i)  # 最近非法右括号边界
            else:
                best = max(best, i - stack[-1])
    return best

07.04 单调队列:固定窗口最大值

239,约定 1≤k≤len(nums)。

复杂度:O(n) 时间、O(k) 辅助,输出 O(n−k+1)。

Python 3 · 核心模板
from collections import deque

def sliding_max(nums, k):
    queue, ans = deque(), []
    for i, value in enumerate(nums):
        while queue and queue[0] <= i - k:
            queue.popleft()
        while queue and nums[queue[-1]] <= value:
            queue.pop()
        queue.append(i)
        if i >= k - 1:
            ans.append(nums[queue[0]])
    return ans
回到原手册 · 栈和队列的题目目录(7 个条目)
PATTERN 08

08 / 链表

识别信号
头节点可能变化、断链与重连、倒数位置、中点、环、相交、归并。
推导顺序
先画局部三到四个节点。明确持有的是节点对象而非值;可能改头就加 dummy;任何改 next 前先保存后继。
核心不变量
反转:prev 是已反转前缀,cur 是未处理头;构造:tail 前的输出已经正确,未处理的链仍然可达。
易错边界
相交比身份 is;快慢指针的初值必须匹配中点用途;倒数 n 约定合法;递归反转用 O(n) 栈;回文检测若改链应按题意恢复。
面试讲法
我用虚拟头统一头部边界,先保存未处理后继,再改变指向,最后接回左右两侧并返回 dummy.next。

原题变式速记:203/19:前驱删除;206/24/25:整体、成对、分组反转;21/148/23:归并、归并排序、堆;2:carry;141/142/287:Floyd;160:换头对齐;234:中点+反转+恢复;138:先建副本再连引用。

08.01 虚拟头 / 整体反转 / 有序归并

依赖公共约定中的 ListNode;merge 原地重用节点。

复杂度:各线性时间、O(1) 辅助。

Python 3 · 核心模板
def remove_nodes(head, target):
    dummy = ListNode(0, head)
    prev = dummy
    while prev.next:
        if prev.next.val == target:
            prev.next = prev.next.next
        else:
            prev = prev.next
    return dummy.next

def reverse_list(head):
    prev, cur = None, head
    while cur:
        nxt = cur.next
        cur.next = prev
        prev, cur = cur, nxt
    return prev

def merge_lists(a, b):
    dummy = tail = ListNode()
    while a and b:
        if a.val <= b.val:
            tail.next, a = a, a.next
        else:
            tail.next, b = b, b.next
        tail = tail.next
    tail.next = a or b
    return dummy.next

08.02 固定间距 / Floyd / 换头对齐

19:1≤n≤长度;142:可有环;160:两条链各自无环。

复杂度:时间 O(n) 或 O(m+n),辅助 O(1)。

Python 3 · 核心模板
def remove_nth_from_end(head, n):
    dummy = ListNode(0, head)
    slow = fast = dummy
    for _ in range(n + 1):
        fast = fast.next
    while fast:
        slow, fast = slow.next, fast.next
    slow.next = slow.next.next
    return dummy.next

def cycle_entry(head):
    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
        if slow is fast:
            seeker = head
            while seeker is not slow:
                seeker, slow = seeker.next, slow.next
            return seeker
    return None

def intersection(a, b):
    p, q = a, b
    while p is not q:
        p = p.next if p else b
        q = q.next if q else a
    return p

08.03 K 组反转:先确认满组再动指针

25,k≥1;不足 k 个保留原顺序;k=2 也覆盖两两交换。

复杂度:O(n) 时间、O(1) 辅助。

Python 3 · 核心模板
def reverse_k_group(head, k):
    dummy = ListNode(0, head)
    group_prev = dummy
    while True:
        kth = group_prev
        for _ in range(k):
            kth = kth.next
            if kth is None:
                return dummy.next
        group_next = kth.next
        prev, cur = group_next, group_prev.next
        while cur is not group_next:
            nxt = cur.next
            cur.next = prev
            prev, cur = cur, nxt
        old_head = group_prev.next
        group_prev.next = kth
        group_prev = old_head

08.04 K 路归并:堆保存每条链的当前头

23:输入各链有序且互不共享节点;同值用序号避免比较节点。

复杂度:总 N 节点、K 条链:O(N log(K+1)) 时间,O(K) 辅助。

Python 3 · 核心模板
import heapq
from itertools import count

def merge_k_lists(lists):
    serial, heap = count(), []
    for head in lists:
        if head:
            heapq.heappush(heap, (head.val, next(serial), head))
    dummy = tail = ListNode()
    while heap:
        _, _, node = heapq.heappop(heap)
        tail.next = node
        tail = node
        if node.next:
            nxt = node.next
            heapq.heappush(heap, (nxt.val, next(serial), nxt))
    if tail is not dummy:
        tail.next = None
    return dummy.next

08.05 两遍深拷贝:对象映射

138:next 是无环链,random 指向链内任意节点或 None。

复杂度:O(n) 时间、O(n) 映射,副本输出另计。

Python 3 · 核心模板
def copy_random_list(head):
    copies = {None: None}
    cur = head
    while cur:
        copies[cur] = RandomNode(cur.val)
        cur = cur.next
    cur = head
    while cur:
        copies[cur].next = copies[cur.next]
        copies[cur].random = copies[cur.random]
        cur = cur.next
    return copies[head]
回到原手册 · 链表的题目目录(15 个条目)
PATTERN 09

09 / 二叉树

识别信号
树的逐层访问、路径约束、左右子树汇总、BST 次序、遍历序列构造。
推导顺序
需要父传子的条件放参数,需要子回答父的信息放返回值。路径能否分叉,决定返回值与全局答案是否不同。
核心不变量
后序 DP 的返回值必须能被父节点继续连接;全局候选可以在本节点连接左右两支。
易错边界
最大路径和全负时不能从 0 初始化;BST 上下界来自全部祖先而非仅父节点;543 直径按边计;236 标准模板依赖两节点存在;构造题通常假设值互异。
面试讲法
dfs 返回从当前节点向下的一条最优可延伸链;左右都算完后,再在当前节点拼接,更新整棵树的答案。

原题变式速记:144/94/145:前中后序;102/199:层序/取每层末;104/226/101/112:契约型递归;98/230:界限/中序;108/105/106:区间分治;236:后序汇总;114:重连;543/124:树形 DP;437:当前根路径上的前缀频次。

09.01 显式遍历 / 层序 / BST 上下界

中序适用于任意二叉树;只有 BST 中序才有序。

复杂度:遍历 O(n);栈 O(h),BFS 队列 O(w);界限递归 O(h)。

Python 3 · 核心模板
from collections import deque

def inorder(root):
    ans, stack, cur = [], [], root
    while cur or stack:
        while cur:
            stack.append(cur)
            cur = cur.left
        cur = stack.pop()
        ans.append(cur.val)
        cur = cur.right
    return ans

def level_order(root):
    if root is None:
        return []
    queue, ans = deque([root]), []
    while queue:
        row = []
        for _ in range(len(queue)):
            node = queue.popleft()
            row.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        ans.append(row)
    return ans

def valid_bst(root):
    def dfs(node, low, high):
        if node is None:
            return True
        return (low < node.val < high
                and dfs(node.left, low, node.val)
                and dfs(node.right, node.val, high))
    return dfs(root, -float('inf'), float('inf'))

09.02 后序汇总:LCA 与单支返回、多支更新

236 假设 p、q 都存在;124 输入树非空。

复杂度:O(n) 时间、O(h) 递归栈;退化树 h=n。

Python 3 · 核心模板
def lowest_common_ancestor(root, p, q):
    if root is None or root is p or root is q:
        return root
    left = lowest_common_ancestor(root.left, p, q)
    right = lowest_common_ancestor(root.right, p, q)
    if left and right:
        return root
    return left or right

def max_path_sum(root):
    best = -float('inf')
    def dfs(node):
        nonlocal best
        if node is None:
            return 0
        left = max(0, dfs(node.left))
        right = max(0, dfs(node.right))
        best = max(best, node.val + left + right)
        return node.val + max(left, right)
    dfs(root)
    return best

def tree_diameter(root):
    best = 0
    def depth(node):
        nonlocal best
        if node is None:
            return 0
        left, right = depth(node.left), depth(node.right)
        best = max(best, left + right)
        return max(left, right) + 1
    depth(root)
    return best

09.03 树路径前缀和:离开时撤销

437 任意向下路径;频次仅表示当前祖先链。

复杂度:期望 O(n) 时间;O(h) 路径频次与栈。

Python 3 · 核心模板
def tree_path_count(root, target):
    freq = {0: 1}
    def dfs(node, prefix):
        if node is None:
            return 0
        prefix += node.val
        ans = freq.get(prefix - target, 0)
        freq[prefix] = freq.get(prefix, 0) + 1
        ans += dfs(node.left, prefix) + dfs(node.right, prefix)
        freq[prefix] -= 1
        if freq[prefix] == 0:
            del freq[prefix]
        return ans
    return dfs(root, 0)

09.04 前序 + 中序:用边界构造,避免切片

105:值互异且两序列合法。106 换后序根时需先构右树再构左树。

复杂度:O(n) 时间;O(n) 索引、O(h) 栈,生成树另计。

Python 3 · 核心模板
def build_pre_in(preorder, inorder_values):
    positions = {x: i for i, x in enumerate(inorder_values)}
    pre_index = 0
    def build(left, right):  # 中序半开区间 [left,right)
        nonlocal pre_index
        if left >= right:
            return None
        value = preorder[pre_index]
        pre_index += 1
        mid = positions[value]
        root = TreeNode(value)
        root.left = build(left, mid)
        root.right = build(mid + 1, right)
        return root
    return build(0, len(inorder_values))
回到原手册 · 二叉树的题目目录(19 个条目)
PATTERN 10

10 / 回溯算法

识别信号
要求输出所有方案、排列组合、切分、约束摆放。
推导顺序
明确搜索树每层决定什么、候选是什么、何时收集、哪些状态需要撤销;先保证不重不漏,再用有依据的剪枝。
核心不变量
进入 dfs 时 path 是合法前缀;返回时共享可变状态恢复到进入前;加入答案必须复制。
易错边界
组合用 start;排列用 used;可重复取数递归传 i,不可重复传 i+1;候选含重复值时层内去重与路径内禁用不同。
面试讲法
我将答案编码为一条选择路径,只扩展合法前缀。递增下标避免组合顺序重复,每次返回恢复现场以保证兄弟分支独立。

原题变式速记:78:每个节点收集;77:长度 k 才收集,并按剩余数量剪枝;46:used;17:位置即层;22:左右括号约束;39:正整数可重复;131:枚举回文段尾;51:列与两条对角线占用。

10.01 子集 / 定长组合 / 排列

三个模型的状态变量不能混用;组合输入 1..n。

复杂度:子集 O(n·2ⁿ)、排列 O(n·n!),含复制输出;组合 O(k·C(n,k)+搜索开销),辅助 O(n)。

Python 3 · 核心模板
def subsets(nums):
    ans, path = [], []
    def dfs(start):
        ans.append(path.copy())
        for i in range(start, len(nums)):
            path.append(nums[i])
            dfs(i + 1)
            path.pop()
    dfs(0)
    return ans

def combinations(n, k):
    ans, path = [], []
    def dfs(start):
        if len(path) == k:
            ans.append(path.copy())
            return
        need = k - len(path)
        for x in range(start, n - need + 2):
            path.append(x)
            dfs(x + 1)
            path.pop()
    dfs(1)
    return ans

def permutations(nums):  # nums 元素互异
    ans, path, used = [], [], [False] * len(nums)
    def dfs():
        if len(path) == len(nums):
            ans.append(path.copy())
            return
        for i, x in enumerate(nums):
            if used[i]:
                continue
            used[i] = True
            path.append(x)
            dfs()
            path.pop()
            used[i] = False
    dfs()
    return ans

10.02 可重复组合 + 合法前缀约束

39 候选为互异正整数;22 生成 n 对括号。

复杂度:组合搜索为指数级,深度≤target/min候选;括号 O(n·Cₙ) 含输出,栈 O(n)。

Python 3 · 核心模板
def combination_sum(candidates, target):
    a = sorted(candidates)
    ans, path = [], []
    def dfs(start, remain):
        if remain == 0:
            ans.append(path.copy())
            return
        for i in range(start, len(a)):
            if a[i] > remain:
                break
            path.append(a[i])
            dfs(i, remain - a[i])  # 同一值可再选
            path.pop()
    dfs(0, target)
    return ans

def generate_parentheses(n):
    ans = []
    def dfs(path, left, right):
        if left == right == n:
            ans.append(path)
            return
        if left < n:
            dfs(path + '(', left + 1, right)
        if right < left:
            dfs(path + ')', left, right + 1)
    dfs('', 0, 0)
    return ans

10.03 切分 / 约束占位

131 先预处理回文表;51 按行放皇后,集合维护列与斜线。

复杂度:回文 O(n²+n·2ⁿ) 含输出;皇后搜索保守 O(n·n!),另加 O(答案数·n²) 输出。

Python 3 · 核心模板
def palindrome_partitions(s):
    n = len(s)
    pal = [[False] * n for _ in range(n)]
    for i in range(n-1, -1, -1):
        for j in range(i, n):
            pal[i][j] = s[i] == s[j] and (j-i < 2 or pal[i+1][j-1])
    ans, path = [], []
    def dfs(start):
        if start == n:
            ans.append(path.copy())
            return
        for end in range(start, n):
            if pal[start][end]:
                path.append(s[start:end+1])
                dfs(end + 1)
                path.pop()
    dfs(0)
    return ans

def n_queens(n):
    cols, diag1, diag2 = set(), set(), set()
    path, ans = [], []
    def dfs(row):
        if row == n:
            ans.append(['.'*c + 'Q' + '.'*(n-c-1) for c in path])
            return
        for c in range(n):
            if c in cols or row-c in diag1 or row+c in diag2:
                continue
            cols.add(c); diag1.add(row-c); diag2.add(row+c)
            path.append(c)
            dfs(row + 1)
            path.pop()
            cols.remove(c); diag1.remove(row-c); diag2.remove(row+c)
    dfs(0)
    return ans
回到原手册 · 回溯算法的题目目录(8 个条目)
PATTERN 11

11 / 递归

识别信号
问题能分解为更小的同类问题,重复状态可复用答案。
推导顺序
先用一句话写函数契约,再写基例、规模减小和返回值合并;有重复子问题且状态完整时加入缓存。
核心不变量
相同参数必须对应相同答案;缓存键包含决定后续结果的全部信息;递归必须有严格缩小的度量。
易错边界
别把调用次数当栈深;缓存不降低深度;不能把依赖共享可变路径的搜索仅按坐标缓存;不使用可变默认缓存跨题残留。
面试讲法
我先定义 f(n) 的返回语义,再验证边界和递推。相同状态只展开一次,因此按状态数乘每状态转移成本计算。

原题变式速记:509:F(0)=0、F(1)=1;70:空楼梯方案 W(0)=1,W(1)=1;两题递推相同但初值不同。递归→记忆化→递推→滚动变量→快速倍增。

11.01 记忆化与滚动递推

缓存写在本次调用内部;n 为非负整数。

复杂度:两者 O(n) 时间;缓存与栈 O(n),滚动 O(1)。

Python 3 · 核心模板
from functools import lru_cache

def fib_memo(n):
    @lru_cache(None)
    def dfs(k):
        if k < 2:
            return k
        return dfs(k-1) + dfs(k-2)
    return dfs(n)

def fib_rolling(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

def climb_stairs(n):
    a, b = 1, 1
    for _ in range(n):
        a, b = b, a + b
    return a

11.02 规模减半:快速倍增

F(2k)=F(k)·[2F(k+1)−F(k)];F(2k+1)=F(k)²+F(k+1)²。

复杂度:O(log n) 次算术步骤、O(log n) 栈;大整数位运算代价另计。

Python 3 · 核心模板
def fib_fast(n):
    def pair(k):
        if k == 0:
            return 0, 1
        a, b = pair(k // 2)
        c = a * (2*b - a)
        d = a*a + b*b
        return (d, c+d) if k % 2 else (c, d)
    return pair(n)[0]
回到原手册 · 递归的题目目录(2 个条目)
PATTERN 12

12 / 深度与广度优先搜索

识别信号
网格或图的连通性、面积、最少步数、同时传播、路径存在。
推导顺序
先确定节点和边,再判断 visited 是永久归属还是当前路径占用;等权最短路用 BFS,所有源同时出发就一起入队。
核心不变量
连通块在发现时永久标记;BFS 队列按距离非递减;路径搜索离开当前分支时恢复占用。
易错边界
入队时标记避免重复;处理层时固定本层长度;普通 BFS 不解决一般带权最短路;深网格递归可能溢出;Python 负下标也必须先拦截。
面试讲法
我把每个合法格看作顶点,四方向相邻为边。发现即标记保证每点一次;若求距离,FIFO 按层处理保证首次到达最短。

原题变式速记:200/695:同一 flood fill,外层启动次数是数量、每次访问数是面积;994:多源 BFS;79:坐标+字符位置+当前路径占用,结束后撤销。

12.01 显式栈连通块:同时统计数量与最大面积

200/695 的统一版:输入为 0/1 整数网格,修改输入;原题 200 字符网格需对应改比较值。

复杂度:O(mn) 时间、最坏 O(mn) 栈。

Python 3 · 核心模板
def island_stats(grid):
    if not grid or not grid[0]:
        return 0, 0
    m, n = len(grid), len(grid[0])
    count = largest = 0
    for r in range(m):
        for c in range(n):
            if grid[r][c] != 1:
                continue
            count += 1
            grid[r][c] = 0
            stack, area = [(r, c)], 0
            while stack:
                x, y = stack.pop()
                area += 1
                for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)):
                    nx, ny = x+dx, y+dy
                    if 0 <= nx < m and 0 <= ny < n and grid[nx][ny] == 1:
                        grid[nx][ny] = 0
                        stack.append((nx, ny))
            largest = max(largest, area)
    return count, largest

12.02 多源分层 BFS

994:所有初始腐烂橘子在时间 0 入队;修改输入。

复杂度:O(mn) 时间、O(mn) 辅助。

Python 3 · 核心模板
from collections import deque

def rotting_oranges(grid):
    if not grid or not grid[0]:
        return 0
    m, n = len(grid), len(grid[0])
    queue = deque()
    fresh = 0
    for r in range(m):
        for c in range(n):
            if grid[r][c] == 2:
                queue.append((r, c))
            elif grid[r][c] == 1:
                fresh += 1
    minutes = 0
    while queue and fresh:
        for _ in range(len(queue)):
            r, c = queue.popleft()
            for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)):
                nr, nc = r+dr, c+dc
                if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == 1:
                    grid[nr][nc] = 2
                    fresh -= 1
                    queue.append((nr, nc))
        minutes += 1
    return minutes if fresh == 0 else -1

12.03 路径 DFS:使用与恢复现场

79 单词搜索;成功返回时也通过 finally 恢复输入。

复杂度:保守 O(mn·4ᴸ),更细上界 O(mn·3ᴸ);递归栈 O(L),L 为词长。

Python 3 · 核心模板
def word_exists(board, word):
    if not word:
        return True
    if not board or not board[0]:
        return False
    m, n = len(board), len(board[0])
    marker = object()
    def dfs(r, c, i):
        if not (0 <= r < m and 0 <= c < n) or board[r][c] != word[i]:
            return False
        if i == len(word)-1:
            return True
        old = board[r][c]
        board[r][c] = marker
        try:
            return any(dfs(r+dr, c+dc, i+1)
                       for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)))
        finally:
            board[r][c] = old
    return any(dfs(r, c, 0) for r in range(m) for c in range(n))
回到原手册 · 深度与广度优先搜索的题目目录(6 个条目)
PATTERN 13

13 / 动态规划

识别信号
最值/计数/可行性;子问题有重叠;状态能够概括未来所需历史。
推导顺序
固定五步:状态精确定义→分类最后一步→初值和非法状态→依赖顺序→答案位置。先写二维或数组版,再按依赖压缩。
核心不变量
每个状态只保留对后续决策等价的历史中最优或汇总结果;更新顺序不得覆盖还会读取的旧状态。
易错边界
前缀最优与结尾最优不同;最大乘积保留最大最小;空前缀计数常为 1、可行为 True、代价为 0;子序列不要求连续;压缩二维要保存旧对角线。
面试讲法
我定义 dp 的完整语义,按最后一次选择把所有方案分成互斥且穷尽的类别;依赖状态已经求出,所以转移正确。

原题变式速记:509/70:相邻递推;118:上一行相邻两项;198:选/不选;53/152:结尾极值;62/63/64:上左依赖;139:最后切分点;300:结尾 DP 或最小尾值二分;1143/72:双前缀。

13.01 一维:选或不选 / 结尾最优 / 双极值

198 允许不选;53/152 输入非空、要求非空子数组。

复杂度:各 O(n) 时间、O(1) 辅助。

Python 3 · 核心模板
def rob(nums):
    prev2 = prev1 = 0
    for x in nums:
        prev2, prev1 = prev1, max(prev1, prev2 + x)
    return prev1

def max_subarray(nums):
    end = best = nums[0]
    for i in range(1, len(nums)):
        end = max(nums[i], end + nums[i])
        best = max(best, end)
    return best

def max_product(nums):
    high = low = best = nums[0]
    for i in range(1, len(nums)):
        x = nums[i]
        old_high, old_low = high, low
        high = max(x, old_high*x, old_low*x)
        low = min(x, old_high*x, old_low*x)
        best = max(best, high)
    return best

13.02 网格:计数 / 障碍 / 最小代价

63 障碍为 1;62 可传全 0 网格;64 为非空数值网格。

复杂度:O(mn) 时间、O(n) 辅助。

Python 3 · 核心模板
def obstacle_paths(grid):
    if not grid or not grid[0]:
        return 0
    dp = [0] * len(grid[0])
    dp[0] = 1
    for row in grid:
        for c, blocked in enumerate(row):
            if blocked:
                dp[c] = 0
            elif c > 0:
                dp[c] += dp[c-1]
    return dp[-1]

def min_path_sum(grid):
    n = len(grid[0])
    dp = [float('inf')] * n
    dp[0] = 0
    for row in grid:
        for c, value in enumerate(row):
            dp[c] = min(dp[c], dp[c-1] if c else float('inf')) + value
    return dp[-1]

def pascal_triangle(rows):
    ans = []
    for i in range(rows):
        row = [1] * (i+1)
        for j in range(1, i):
            row[j] = ans[-1][j-1] + ans[-1][j]
        ans.append(row)
    return ans

13.03 前缀划分 / 严格递增子序列

139:dp[i] 是 s[:i] 可拆分;300:tails 保存每个长度的最小结尾。

复杂度:拆词候选最大长 L:Python 切片+哈希 O(nL²) 上界,空间含词表与 O(n) DP;LIS O(n log n)/O(n)。

Python 3 · 核心模板
from bisect import bisect_left

def word_break(s, words):
    vocab = set(words)
    limit = max(map(len, vocab), default=0)
    dp = [False] * (len(s)+1)
    dp[0] = True
    for i in range(1, len(s)+1):
        for j in range(max(0, i-limit), i):
            if dp[j] and s[j:i] in vocab:
                dp[i] = True
                break
    return dp[-1]

def lis_length(nums):
    tails = []
    for x in nums:
        i = bisect_left(tails, x)  # 严格递增:首个 >= x
        if i == len(tails):
            tails.append(x)
        else:
            tails[i] = x
    return len(tails)  # tails 本身未必是原序列的一个子序列

13.04 双前缀:LCS / 编辑距离

1143 用 max 选最长;72 用 min 选最小操作代价;允许空串。

复杂度:O(mn) 时间、O(n) 辅助。

Python 3 · 核心模板
def lcs_length(a, b):
    dp = [0] * (len(b)+1)
    for x in a:
        diagonal = 0
        for j, y in enumerate(b, 1):
            old_up = dp[j]
            if x == y:
                dp[j] = diagonal + 1
            else:
                dp[j] = max(dp[j], dp[j-1])
            diagonal = old_up
    return dp[-1]

def edit_distance(a, b):
    dp = list(range(len(b)+1))
    for i, x in enumerate(a, 1):
        diagonal = dp[0]
        dp[0] = i
        for j, y in enumerate(b, 1):
            old_up = dp[j]
            dp[j] = diagonal if x == y else 1 + min(diagonal, dp[j], dp[j-1])
            diagonal = old_up
    return dp[-1]
回到原手册 · 动态规划的题目目录(13 个条目)

06 / 用三道题练习“组合模板”

560 和为 K 的子数组

暴力枚举区间 O(n²) → P[r]−P[l]=k → 查历史 P[r]−k 的次数 → 期望 O(n)。

推演 [1,−1,1]、k=1:freq={0:1};前缀 1 命中 1 次;前缀 0 命中 0 次并把 0 次数改为 2;前缀 1 命中 2 次;总计 3。

239 滑动窗口最大值

窗口右移要支持删除旧最大值 → 单个 max 变量不够 → 保存候选下标 → 淘汰过期头和被新元素支配的尾。

[1,3,−1]、k=2:0 入队;1 到来淘汰 0,输出 3;2 入队形成 [1,2],输出 3。值可重复时保留更新的下标更耐过期。

437 路径总和 III

把数组前缀哈希迁移到树 DFS → 表中只允许当前祖先路径 → 进入登记,左右子树结束后减回。

数组扫描只向前,不撤销;树会进入兄弟分支,必须撤销。若不撤销,就会把不在同一条路径上的节点错误组合。

一题多解时的取舍

先给满足约束的清晰解,再解释优化:300 可先讲 O(n²) 结尾 DP,再讲 O(n log n) 最小尾值;509 先记忆化,再滚动;23 对 K 路选堆或分治。

替换条件时重新判断:窗口遇负数、股票加手续费、BFS 边权不同、回溯候选可重复,均需要重新建模。

07 / 笔试:把算法做成可提交程序

开始前:扫一遍题与接口

按自己能稳定完成的顺序安排。记录总时长、每题分值、输入规模、测试组数量;确认函数模式还是 ACM 模式,以及是否允许空数组和负数。

卡住时:设一个决策点

连续数分钟没有新信息时,写下当前暴力和瓶颈;切另一题或换建模角度。若是部分分赛制,先交正确可运行的基线,再根据剩余时间优化。

提交前:检查真实代价

避免循环里切片、字符串累加和 list.pop(0) 放大复杂度。递归深度可能随 n 增长时改显式栈或迭代;评估 O(n²) 表的实际内存。

提交后:按症状定位

WA 先查边界/重复/更新顺序;TLE 数总操作和解析成本;RE 查空值/越界/递归深度;MLE 查二维表/缓存/结果保存。保留能复现的最小失败用例。

ACM 输入输出骨架(示例:T 组,每组 n、target、随后 n 个整数)

这里只演示“两数之和”的输入格式,返回零基下标;无解输出 -1 -1。实际题目若采用一基下标、单组输入、字符串含空格或不同输出要求,必须按题面修改。此块独立可运行。

Python 3 · 核心模板
import sys

def solve_case(nums, target):
    seen = {}
    for i, x in enumerate(nums):
        if target - x in seen:
            return seen[target - x], i
        seen[x] = i
    return -1, -1

def main():
    tokens = iter(map(int, sys.stdin.buffer.read().split()))
    cases = next(tokens, None)
    if cases is None:
        return
    output = []
    for _ in range(cases):
        n, target = next(tokens), next(tokens)
        nums = [next(tokens) for _ in range(n)]
        i, j = solve_case(nums, target)
        output.append(f'{i} {j}')
    sys.stdout.write('\n'.join(output))

if __name__ == '__main__':
    main()

输入 2 / 4 9 / 2 7 11 15 / 3 6 / 3 2 4(斜线表示换行),输出两行:0 1、1 2。大输入可按行迭代以减少一次性 token 列表内存;该骨架只适合空白分隔整数。

08 / 面试:用同一套结构讲清所有题型

可直接练习的表达模板:
“我先确认____约束。直接枚举需要____。这里重复的是____,或存在____性质,所以使用____。我让____表示____,每次____并保证____。边界是____,答案在____更新。正确性来自____。总时间____,辅助空间____;若条件改为____,需要重新处理____。”
  1. 确认:用一个例子复述输入输出;主动澄清连续、重复、是否改输入、节点是否存在等关键条件。
  2. 基线:给出正确的直接做法和瓶颈,让优化有具体对象。
  3. 建模:写清状态/不变量、移动规则/转移和终止条件,再编码。
  4. 证明:双指针讲排除候选;贪心讲交换/支配/上界;DP 讲分类完备;BFS 讲层序;回溯讲不重不漏与恢复。
  5. 验证:手推一个普通例和一个边界例;说明时间、辅助空间和输出空间。
  6. 追问:如果要输出具体方案,增加前驱或选择记录;若禁改输入,使用额外存储;若数据流或大量查询,重新评估预处理与维护结构。

六类常见追问,应该补充什么?

追问回答重点
为什么两个循环还是 O(n)?每个指针最多推进 n 次,或每个元素只入出容器一次,按总次数分析。
为什么不能再少一个状态?给出状态不足的反例:152 负数会把最小乘积变成最大;124 双支不能向父继续连。
缓存为什么正确?相同键具有相同后续答案;状态包含所有相关历史;统计唯一状态及转移数。
能不能 O(1) 空间?先说明空间口径。输出数组、递归栈、Python 排序、输入修改都分别交代。
为什么剪枝不漏答案?证明候选已不可能满足约束;例如正候选排序后大于 remain,后续也不可能。
代码与口述不一致怎么办?回到不变量和一个最小用例,定位具体边界或更新顺序,修正后重推,不凭感觉改等号。

09 / 提交前的边界检查表

类别最小测试要抓的错误
数组 / 字符串[]、[x]、全相等、严格增减、全负、多个 0仅在接口允许时测空输入;检查非空答案是否错误初始化为 0。
窗口 / 前缀k=1、k=n、无解、从下标0开始、[0,0] 且 k=0最后一个答案数为 3;抓空前缀、频次与先查再存。
二分空数组、目标在两端、目标缺失、全是目标返回 n 后不能直接索引;区分最后位置与右开边界。
单调结构[2,2]、单调增减、k=1、k=n严格与非严格比较、过期下标、矩形宽度。
链表空链、单节点、删头/删尾、自环、相同值但不同对象dummy.next、断链、身份判断、环导致的死循环。
树空树、单节点、退化链、全负节点、目标在同侧递归深度、返回值语义、空树定义、全局答案初值。
图 / 网格全水、全陆、长蛇、不可达新鲜橘子、无新鲜橘子发现即标记;无新鲜返回0,不可达返回−1。
回溯 / DP无解、唯一解、重复候选、空前缀、仅负数结尾答案复制、状态撤销、初值、滚动覆盖和循环方向。

10 / 选型自测:先说理由再点答案

题目用于检查条件判断;答题不计入专题掌握进度。

1. 任意整数数组,统计和恰为 k 的连续子数组个数。

2. 无权网格,多个源同时扩散,求全部到达的最少时间。

3. 全排列里某个位置已用过,另一条分支能否再使用?

4. 最大路径和向父节点应该返回什么?

5. 升序 [2,2,2],首个严格大于2的位置?

6. 股票任意次交易,但每笔有手续费,还能直接加所有正差吗?

11 / 面向秋招的复习安排

按可投入时间缩放;每天保留“闭卷写 → 解释 → 改错 → 隔天重写”。这里按现有题集安排,不把“看懂题解”算作掌握。

阶段练习主题必须能独立完成的检查
第1–2天双指针、哈希、前缀1 / 15 / 26 / 560,解释先查再存、排序是否影响下标。
第3–4天窗口、二分3 / 76 / 34 / 33,写出更新答案时机和边界不变量。
第5–6天栈、队列、贪心20 / 739 / 239 / 55 / 45,说明均摊分析与局部决策依据。
第7–8天链表、二叉树206 / 19 / 142 / 102 / 236 / 124,画指针,解释返回值。
第9–10天递归、DFS/BFS、回溯200 / 994 / 79 / 78 / 46,区分永久标记与路径撤销。
第11–12天一维、网格、序列 DP198 / 53 / 63 / 139 / 300 / 1143 / 72,独立推状态与初值。
第13天混合模拟笔试按自己的笔试时长选2–4题,使用真实输入输出,无分类提示。
第14天错题重写 + 模拟面试优先重做暴露建模问题的题,每题先讲方案与证明再编码。

只有 3 天时

第1天:1、3、34、560;第2天:206、102、200、994、46;第3天:198、53、300,外加一次限时混合题与一次口述。已熟练的题替换为相应变式,避免只背原输入。

错题记录只写五项

触发信号 → 错误假设 → 正确不变量/状态 → 最小反例 → 下次验证点。例如:“连续和 → 误以为有负数也能缩窗 → 改用历史前缀频次 → [1,−1,1] → 查频次且先查再存”。隔天、3天后、7天后闭卷再写,间隔可按掌握程度调整。

掌握标准:在没有题型提示时选对方法;能写完整代码;能说明为何正确及复杂度;能处理一个条件变化。只有这四项都做到,再勾选章节掌握。

12 / 原手册导航与资料边界

本页依据本地 1–13 HTML 的题目、选型总结与核心模板整理。原来未编号的链表页按专题顺序归为 08;排序算法原已是中文名,保留为补充入口。本页新增的是跨专题选型、统一函数模板、提交与面试流程。

原题的完整题意、逐题推演、多解比较及交互练习继续查看原手册。本文覆盖的是这 13 个专题的应试总结;若目标岗位题库涉及堆、并查集、拓扑排序、带权最短路、背包、Trie、位运算等独立专题,需要另补专项练习。

搜索、主题切换、速记、自测、代码复制与打印均在本地工作,不请求网络。勾选进度仅保存在当前浏览器,file:// 下的存储支持取决于浏览器;禁用存储时本次会话仍可使用。

回到顶部 ↑