Skip to content

动态规划与贪心 ​

动态规划:先定义状态,再写转移 ​

动态规划适用于有重叠子问题和最优子结构的问题。写题顺序:

  1. 定义 dp 状态:下标、含义、单位;
  2. 写出状态转移,说明每个分支代表什么选择;
  3. 初始化最小规模状态;
  4. 确认遍历顺序保证依赖状态已计算;
  5. 用边界输入验证,并判断是否可滚动压缩空间。

不要只背公式。dp[i] 可能代表“以 i 结尾的最优值”,也可能代表“前 i 个元素的全局最优值”;二者的答案位置和转移不同。

一维序列 ​

cpp
// 最大子数组和:best_end 表示必须以当前元素结尾的最大和。
int best_end = nums.front();
int answer = best_end;
for (std::size_t i = 1; i < nums.size(); ++i) {
    best_end = std::max(nums[i], best_end + nums[i]);
    answer = std::max(answer, best_end);
}

斐波那契、爬楼梯、打家劫舍等常只依赖有限前项,可把 O(n) DP 数组压缩为常数变量。应先处理空数组和极小长度。

0/1 背包与完全背包 ​

令 dp[capacity] 表示当前处理到的物品能取得的最优值:

cpp
// 0/1 背包:每件最多一次,容量必须倒序。
for (int item = 0; item < n; ++item)
    for (int cap = capacity; cap >= weight[item]; --cap)
        dp[cap] = std::max(dp[cap], dp[cap - weight[item]] + value[item]);

// 完全背包:每件可重复,容量正序。
for (int item = 0; item < n; ++item)
    for (int cap = weight[item]; cap <= capacity; ++cap)
        dp[cap] = std::max(dp[cap], dp[cap - weight[item]] + value[item]);

循环方向控制是否会在同一轮复用当前物品。计数问题还要区分:先枚举物品通常统计组合,先枚举容量再枚举物品通常统计排列;具体以题目是否区分顺序为准。

求最小值时将不可达状态初始化为足够大的 INF,并在转移前确认前置状态可达;求方案数时留意 long long 或取模,不能用溢出后的 int 当逻辑状态。

双序列、区间与树形 DP ​

  • LCS:dp[i][j] 表示两个前缀的最长公共子序列;字符相等取左上加一,否则取上/左最大;
  • 编辑距离:字符不同对应替换、删除、插入三种前置状态中的最小值加一;首行首列初始化为与空串互转的操作数;
  • 回文区间:常用 dp[left][right],需要先算更短内部区间,所以 left 通常倒序或按区间长度枚举;
  • 区间 DP:如戳气球,枚举区间与“最后执行”的分割点;
  • 股票状态机:将“第几天、剩余交易次数、是否持有”作为状态,先定义交易次数在买入还是卖出时消耗,避免下标混乱;
  • 树形 DP:后序返回每个节点的有限状态,让父节点合并子节点结果。

贪心:需要可证明的局部选择 ​

贪心不是“每步看起来最好”就成立,而是必须能用交换论证、保持领先或不变量证明局部选择不会损失全局最优。

常见可靠模式:

问题贪心选择
选择最多不重叠区间总是选结束时间最早的可选区间
跳跃游戏 I扫描时维护目前可达的最远下标
跳跃游戏 II在当前可达区间内维护下一跳最远边界
分发饼干两边排序,用最小可满足饼干匹配最小未满足需求
分发糖果左右各扫描一次,逐位置取两侧约束最大值
划分字母区间预处理每字符最后出现位置,扫描维护当前段最远边界

区间贪心大多需要排序,但排序键由证明决定:求最多不重叠通常按结束时间,合并区间通常按开始时间。不要看到区间就统一套同一种排序。

如何选择:DP、贪心还是搜索 ​

text
要求所有方案 / 约束组合 → 回溯、状态压缩或 DP
存在可证明的局部最优规则 → 贪心
最优值由多个历史状态决定 → DP
有序或答案空间单调 → 二分
图上的可达性 / 连通块 / 最短层数 → DFS、BFS、并查集

面试表达建议:先说状态或不变量,再给转移/选择理由、复杂度、边界条件,最后说明空间优化是否必要。这样比只报出题号或模板更能展示可迁移的解题能力。

关联笔记 ​

使用 Markdown 与 VitePress 构建