GitHub链接:algorithmzuo (左程云) · GitHub
解题流程:
1.尝试,进行逻辑分析,分类讨论,观察规律,从简单到复杂,正难求反,利用答案的集合性质等等,观察数据量。
2.写出递归
3.挂缓存,改成记忆化搜索
4.根据记忆化搜索写出严格位置依赖版本的dp
5.画图,举例子,建立空间感,构建空间压缩版本。
常见dp
1.背包dp
背包动态规划(DP)是动态规划中的经典问题,其核心在于在有限容量的背包中选择物品以达到最优解(如最大价值、最小成本等)。以下是常见的背包问题类型及其解法总结:
1. 0-1 背包问题
问题描述:
- 给定物品的重量
w[i]和价值v[i],每个物品只能选或不选(0或1),求容量为W的背包能装的最大价值。
解法:
- 状态定义:
dp[i][j]表示前i个物品在容量j时的最大价值。 - 转移方程:
- 不选第
i个物品:dp[i][j] = dp[i-1][j] - 选第
i个物品:dp[i][j] = dp[i-1][j-w[i]] + v[i]
- 不选第
- 优化:
- 空间压缩到一维数组:
dp[j] = max(dp[j], dp[j-w[i]] + v[i]),需逆序更新j(从W到w[i])。
- 空间压缩到一维数组:
例题:
- LeetCode 416. 分割等和子集(转化为背包问题)
2. 完全背包问题
问题描述:
- 物品可以选无限次,其他条件同 0-1 背包。
解法:
- 转移方程:
dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i])(注意i不变,表示可重复选)。
- 优化:
- 一维数组正序更新
j(从w[i]到W)。
- 一维数组正序更新
例题:
- LeetCode 322. 零钱兑换(最小硬币数)
3. 多重背包问题
问题描述:
- 物品
i最多选s[i]次。
解法:
- 二进制拆分:将
s[i]拆成1, 2, 4, ..., 2^k, s[i]-2^k的组合,转化为 0-1 背包。 - 单调队列优化:时间复杂度优化到
O(NW)(较复杂)。
例题:
- 经典问题:物品有限次数的背包最大价值。
4. 分组背包问题
问题描述:
- 物品分为若干组,每组只能选一个物品。
解法:
- 状态转移:
- 对每组枚举所有物品:
dp[j] = max(dp[j], dp[j-w[k]] + v[k]),其中k是当前组的物品。
- 对每组枚举所有物品:
例题:
5. 二维费用背包
问题描述:
- 背包有两种容量限制(如重量和体积),物品对应两种费用。
解法:
- 状态扩展:
dp[i][j][k]表示前i个物品在两种容量j和k时的最优解。 - 优化为二维数组:
dp[j][k] = max(dp[j][k], dp[j-w1[i]][k-w2[i]] + v[i])。
例题:
- LeetCode 474. 一和零(字符串的 0 和 1 数量限制)
6. 背包问题求方案数
问题描述:
- 求装满背包的方案数(不要求最优解)。
解法:
- 状态定义:
dp[j]表示容量j的方案数。 - 转移方程:
dp[j] += dp[j-w[i]](初始dp[0]=1)。
例题:
- LeetCode 494. 目标和(转化为背包方案数)
7. 背包问题求具体方案
问题描述:
- 要求输出最优解的具体物品选择方案。
解法:
- 回溯法:记录状态转移路径,从
dp[N][W]倒推物品选择。 - 标记数组:在 DP 过程中记录是否选择了当前物品。
通用技巧
- 初始化:
- 要求恰好装满:
dp[0]=0,其他初始化为-∞。 - 不要求装满:
dp[0...W]=0。
- 要求恰好装满:
- 遍历顺序:
- 0-1 背包:逆序更新容量。
- 完全背包:正序更新容量。
- 问题转化:将非背包问题(如子集和、字符串匹配)转化为背包模型。
通过掌握这些类型和对应的状态转移方程,可以解决大多数背包 DP 问题。建议从 0-1 背包和完全背包入手,逐步扩展到其他变种。
2.状压dp
状压动态规划(状态压缩 DP)通常用于处理状态包含多个维度且每个维度是二元选择的问题(如“选/不选”、“存在/不存在”)。其核心是通过二进制数压缩状态,从而高效地表示和转移状态。以下是常见的状压 DP 类型及解法总结:
1. 旅行商问题(TSP)
问题描述:
- 给定一个带权无向图,求从起点出发经过所有点恰好一次并回到起点的最短路径。
状态设计:
dp[mask][u]:表示当前已访问的节点集合为mask(二进制位表示),且当前位于节点u时的最短路径长度。- 初始化:
dp[1 << start][start] = 0(起点初始距离为 0)。 - 转移方程:
- 对于所有未访问的节点
v,更新dp[mask | (1 << v)][v] = min(dp[mask][u] + dist[u][v])。
- 对于所有未访问的节点
- 答案:
dp[(1 << n) - 1][start](所有点都访问过,并回到起点)。
优化:
- 预处理两点间最短距离(如 Floyd 算法)。
- 使用记忆化搜索减少重复计算。
例题:
2. 棋盘覆盖/放置问题
问题描述:
- 在棋盘上放置棋子(如车、皇后、多米诺骨牌),要求满足某些限制(如不攻击、不重叠等),求方案数或最大收益。
状态设计:
dp[i][mask]:表示处理到第i行时,当前行的状态为mask(二进制位表示是否放置)。- 转移方程:
- 枚举上一行的状态
prev_mask,检查是否冲突(如列冲突、对角线冲突)。 dp[i][mask] += dp[i-1][prev_mask](合法转移)。
- 枚举上一行的状态
例题:
- LeetCode 52. N 皇后 II(状压枚举皇后位置)
- LeetCode 1349. 参加考试的最大学生数(棋盘放置学生,不相邻)
3. 子集 DP(枚举子集)
问题描述:
- 对集合的所有子集进行 DP 计算(如子集和、子集覆盖等)。
状态设计:
dp[mask]:表示当前子集mask的最优解(如最小操作数、最大价值等)。- 转移方程:
- 枚举
mask的所有子集submask,更新dp[mask] = min(dp[mask], dp[submask] + cost)。
- 枚举
优化:
- 枚举子集技巧:
for (int submask = mask; submask; submask = (submask - 1) & mask) - 预处理:提前计算某些子集的性质(如是否合法)。
例题:
4. 位运算优化 DP
问题描述:
- 利用位运算(如
AND、OR、XOR)进行状态转移,常用于位操作相关的最优化问题。
状态设计:
dp[mask]:表示当前位状态mask的最优解。- 转移方程:
- 通过位运算更新状态,如
dp[mask | new_bits] = min(dp[mask] + cost)。
- 通过位运算更新状态,如
例题:
- LeetCode 847. 访问所有节点的最短路径(TSP 变种,BFS + 状压)
- LeetCode 1125. 最小的必要团队(技能覆盖问题)
5. 轮廓线 DP(插头 DP)
问题描述:
- 处理网格图上的路径/连通性问题(如哈密顿路径、括号匹配等),适用于逐格转移的问题。
状态设计:
dp[i][j][mask]:表示处理到(i, j)时,轮廓线状态为mask(记录插头信息)。- 转移方程:
- 根据当前格是否放置、是否连通等更新
mask。
- 根据当前格是否放置、是否连通等更新
例题:
通用技巧
- 状态压缩方式:
- 用
int或long long的二进制位表示状态(mask & (1 << i)检查第i位是否选中)。
- 用
- 初始化与边界:
- 初始状态通常为
dp[0][...] = 0或dp[1 << start][...] = 0。
- 初始状态通常为
- 优化方法:
- 预处理合法状态(如
mask不能有相邻的 1)。 - 滚动数组优化空间(如
dp[2][mask])。
- 预处理合法状态(如
- 时间复杂度:
- 通常为
O(n * 2^n)(适用于n ≤ 20的情况)。
- 通常为
典型例题
| 类型 | 例题 |
|---|---|
| TSP 问题 | LeetCode 943. 最短超级串 |
| 棋盘放置 | LeetCode 1349. 最大学生数 |
| 子集 DP | LeetCode 698. 划分为 k 个子集 |
| 位运算 DP | LeetCode 847. 访问所有节点的最短路径 |
| 插头 DP | HDU 1693 Eat the Trees |
掌握这些类型后,可以解决大多数状压 DP 问题。建议从 TSP 问题 和 棋盘放置问题 入手,熟悉状态压缩的基本思路。
3.数位dp
数位动态规划(Digit DP)用于解决与数字各位数字相关的计数或最优化问题,通常涉及数字的位数限制、数字性质(如回文、数位和)、区间统计等。以下是常见的数位 DP 类型及解法总结:
1. 基本数位 DP(统计满足条件的数字个数)
问题描述:
- 给定区间
[L, R],统计满足某种条件的数字个数(如不含4、数位递增等)。
解法:
- 状态设计:
dp[pos][state][limit]:pos:当前处理到数字的第pos位(从高位到低位)。state:记录前几位的影响(如前缀和、前导零等)。limit:是否受数字上限约束(如R=123,前两位是12时第三位不能超过3)。
- 转移方程:
- 枚举当前位的数字
d(0到9,受limit限制)。 - 根据
state更新状态(如数位和、是否出现某数字)。
- 枚举当前位的数字
- 记忆化搜索:
- 递归实现,记忆化
limit=false的状态(避免重复计算)。
- 递归实现,记忆化
例题:
2. 数位和问题
问题描述:
- 统计区间
[L, R]内数位和满足条件的数字(如和等于K、是K的倍数等)。
解法:
- 状态扩展:在
state中记录当前数位和sum。 - 剪枝优化:若
sum超过目标或无法达到目标,提前终止。
例题:
- LeetCode 1088. 易混淆数 II(数位和+数字旋转)
- SPOJ SUMTRIAN(数位和限制)
3. 数字性质问题(回文、单调性等)
问题描述:
- 统计满足特定性质的数字(如回文数、数位递增/递减等)。
解法:
- 状态设计:
- 回文数:记录前一半数字,检查是否对称。
- 单调递增:记录前一位数字,确保当前位不小于前一位。
例题:
- LeetCode 902. 最大为 N 的数字组合(单调递增)
- LeetCode 1067. 范围内的数字计数(回文数统计)
4. 数字与模数问题
问题描述:
- 统计数字模
K等于特定值的数字个数(如%K=0)。
解法:
- 状态扩展:在
state中记录当前数字模K的值mod。 - 转移方程:
new_mod = (mod * 10 + d) % K,更新状态。
例题:
- LeetCode 1397. 找到所有好字符串(数位 DP + KMP)
- Codeforces 55D. Beautiful numbers(模数+数位 DP)
5. 数字转换问题(二进制、特殊进制)
问题描述:
- 处理非十进制数字(如二进制、三进制)的统计问题。
解法:
- 调整数位枚举范围(如二进制枚举
0或1)。 - 状态设计与十进制类似,但需注意进制转换。
例题:
- LeetCode 600. 不含连续 1 的非负整数(二进制数位 DP)
- LeetCode 902. 最大为 N 的数字组合(任意进制)
通用技巧
- 问题转化:
- 将
[L, R]的问题转化为[0, R] - [0, L-1]。
- 将
- 状态设计原则:
- 必须包含当前位
pos 和约束状态limit。 - 根据问题添加额外状态(如
sum、mod、前导零lead)。
- 必须包含当前位
- 记忆化优化:
- 仅记忆化
limit=false的状态(limit=true的状态只会计算一次)。
- 仅记忆化
- 边界处理:
- 注意
L=0或R=1e18等特殊情况。
- 注意
模板代码(记忆化搜索)
def digit_dp(num):
s = str(num)
n = len(s)
@cache
def dfs(pos, state, limit, lead):
if pos == n:
return 1 if state_valid(state) else 0 # 根据问题调整
res = 0
up = int(s[pos]) if limit else 9
for d in range(0, up + 1):
new_limit = limit and (d == up)
new_lead = lead and (d == 0)
if new_lead:
res += dfs(pos + 1, state, new_limit, new_lead)
else:
new_state = update_state(state, d) # 根据问题更新状态
res += dfs(pos + 1, new_state, new_limit, new_lead)
return res
return dfs(0, init_state, True, True)
典型例题
| 类型 | 例题 |
|---|---|
| 基本计数 | LeetCode 233. 数字 1 的个数 |
| 数位和 | SPOJ SUMTRIAN |
| 回文数 | LeetCode 1067. 数字计数 |
| 模数问题 | Codeforces 55D. Beautiful numbers |
| 二进制 DP | LeetCode 600. 不含连续 1 的整数 |
掌握这些类型后,可以解决大多数数位 DP 问题。建议从基本计数问题(如统计不含 4 的数字)入手,逐步扩展到复杂状态设计(如模数、数位和)。
4.区间dp
区间动态规划(Interval DP)是一种用于解决区间划分、合并、最优解问题的动态规划方法,其核心思想是通过枚举区间的分割点,逐步求解更大区间的最优解。以下是常见的区间 DP 类型及解法总结:
1. 区间最值/计数问题
问题描述:
- 给定一个序列(如数组、字符串),求满足条件的区间的最值(如最大值、最小值)或方案数。
解法:
- 状态定义:
dp[i][j]:表示区间[i, j]的最优解或方案数。
- 转移方程:
- 枚举分割点
k(i ≤ k < j),将区间[i, j]拆分为[i, k]和[k+1, j],合并结果。 - 例如:
dp[i][j] = max(dp[i][k] + dp[k+1][j] + cost(i, j, k))。
- 枚举分割点
例题:
- LeetCode 312. 戳气球(区间 DP 经典问题)
- LeetCode 516. 最长回文子序列(区间 DP 求回文子序列)
2. 区间合并问题(合并代价最小化)
问题描述:
- 将多个区间合并为一个,每次合并相邻区间需支付一定代价,求最小总代价。
解法:
- 状态定义:
dp[i][j]:表示合并区间[i, j]的最小代价。
- 转移方程:
dp[i][j] = min(dp[i][k] + dp[k+1][j] + sum(i, j)),其中sum(i, j)是区间[i, j]的权重和。
例题:
- LeetCode 1000. 合并石头的最低成本(K 堆合并问题)
- 洛谷 P1880 石子合并(环形区间 DP)
3. 区间划分问题(分割为若干子区间)
问题描述:
- 将一个序列划分为若干子区间,每个子区间需满足特定条件(如和不超过
K),求最小划分数或最大收益。
解法:
- 状态定义:
dp[i]:表示前i个元素的最优解(通常一维即可)。
- 转移方程:
dp[i] = min(dp[j] + cost(j+1, i)),其中j < i且[j+1, i]满足条件。
例题:
- LeetCode 132. 分割回文串 II(最少分割次数)
- LeetCode 1278. 分割回文串 III(修改字符使回文)
4. 区间匹配问题(括号、回文等)
问题描述:
- 处理括号匹配、回文子序列等需要对称性的区间问题。
解法:
- 状态定义:
dp[i][j]:表示区间[i, j]是否匹配或最长匹配长度。
- 转移方程:
- 若
s[i]和s[j]匹配(如括号或相同字符),则dp[i][j] = dp[i+1][j-1] + 2。 - 否则,
dp[i][j] = max(dp[i+1][j], dp[i][j-1])。
- 若
例题:
- LeetCode 5. 最长回文子串(区间 DP 求最长回文子串)
- LeetCode 678. 有效的括号字符串(带通配符的括号匹配)
5. 环形区间 DP(破环成链)
问题描述:
- 当序列是环形(如首尾相连)时,需特殊处理。
解法:
- 破环成链:将原数组复制一份接在后面(
nums + nums),然后对2n长度的序列做区间 DP。 - 最终答案:枚举所有可能的起点
i,取dp[i][i+n-1]的最优解。
例题:
- 洛谷 P1880 石子合并(环形石子合并)
- LeetCode 1547. 切棍子的最小成本(环形切割问题)
通用技巧
- 遍历顺序:
- 区间 DP 通常按区间长度从小到大枚举(先算小区间,再算大区间)。
- 初始化:
- 单个元素的区间
dp[i][i]通常有初始值(如回文长度为 1,合并代价为 0)。
- 单个元素的区间
- 时间复杂度优化:
- 四边形不等式优化(将
O(n^3)优化到O(n^2))。
- 四边形不等式优化(将
- 空间优化:
- 滚动数组(如
dp[2][n])或对角线遍历(减少空间占用)。
- 滚动数组(如
模板代码(区间 DP 框架)
def interval_dp(s):
n = len(s)
dp = [[0] * n for _ in range(n)]
# 初始化单个字符或小区间
for i in range(n):
dp[i][i] = initial_value
# 枚举区间长度
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
# 枚举分割点
for k in range(i, j):
dp[i][j] = update(dp[i][j], dp[i][k] + dp[k+1][j] + cost(i, j, k))
return dp[0][n-1]
典型例题
| 类型 | 例题 |
|---|---|
| 区间最值 | LeetCode 312. 戳气球 |
| 区间合并 | LeetCode 1000. 合并石头的最低成本 |
| 区间划分 | LeetCode 132. 分割回文串 II |
| 区间匹配 | LeetCode 5. 最长回文子串 |
| 环形 DP | 洛谷 P1880 石子合并 |
掌握这些类型后,可以解决大多数区间 DP 问题。建议从石子合并问题和戳气球问题入手,熟悉区间 DP 的基本思路。
5.树型dp
树型动态规划(Tree DP)是一种基于树结构的动态规划方法,常用于处理树上的最优解、计数、路径统计等问题。以下是常见的树型 DP 类型及解法总结:
1. 基础树型 DP(子树统计)
问题描述:
- 统计树中每个子树的性质(如节点数、最大值、和等)。
解法:
- 状态定义:
dp[u]:表示以节点u为根的子树的最优解或统计值。
- 转移方程:
- 递归遍历子树,合并子节点的结果:
dp[u] = combine(dp[v1], dp[v2], ...)。
- 递归遍历子树,合并子节点的结果:
- 遍历方式:
- 后序遍历(先处理子节点,再处理父节点)。
例题:
- LeetCode 543. 二叉树的直径(求最长路径)
- LeetCode 124. 二叉树中的最大路径和(路径和最大)
2. 树上背包问题(分组依赖)
问题描述:
- 在树中选择节点(如保留/删除),满足某些依赖关系(如选子节点必须选父节点),求最大/最小价值。
解法:
- 状态定义:
dp[u][k]:表示以u为根的子树中选择k个节点的最优解。
- 转移方程:
- 类似分组背包,枚举子节点的分配数量:
for v in children[u]: for j in range(k, 0, -1): # 逆序避免重复计数 for t in range(1, j): # 分配给子节点 v 的容量 dp[u][j] = max(dp[u][j], dp[u][j-t] + dp[v][t])
- 类似分组背包,枚举子节点的分配数量:
- 优化:
- 如果树是二叉树,可以用左右子树分开处理。
例题:
- LeetCode 337. 打家劫舍 III(树上不相邻节点最大和)
- 洛谷 P2014 选课(依赖背包)
3. 换根 DP(二次扫描)
问题描述:
- 对每个节点作为根时,求整棵树的性质(如节点到其他节点的距离和、最大值等)。
解法:
- 第一次 DFS:计算以某个根(如
u=0)为基准的子树信息(如dp[u])。 - 第二次 DFS:通过父节点的信息推导其他节点的值:
dp[v] = dp[u] - contribution(v) + new_contribution(v)。
例题:
- LeetCode 834. 树中距离之和(换根求距离和)
- LeetCode 310. 最小高度树(换根找最优根)
4. 树的路径问题(最长/最短路径)
问题描述:
- 求树中满足条件的路径(如最长路径、路径和等于
K的路径数等)。
解法:
- 状态定义:
dp[u][0/1]:记录以u为根的子树的最长路径(可能需要分是否拐弯)。
- 转移方程:
- 最长路径可能由两条子路径拼接而成:
dp[u][0] = max(dp[v][0] + w) # 不拐弯的路径 dp[u][1] = max(dp[u][0], dp[v1][0] + dp[v2][0] + w1 + w2) # 拐弯路径
- 最长路径可能由两条子路径拼接而成:
例题:
5. 树的染色问题(相邻节点限制)
问题描述:
- 给树节点染色,相邻节点有颜色限制(如不同色),求方案数或最小成本。
解法:
- 状态定义:
dp[u][c]:表示节点u染颜色c时的方案数或成本。
- 转移方程:
- 枚举子节点的颜色(不与父节点冲突):
for v in children[u]: for c_child in colors: if c_child != c: dp[u][c] += dp[v][c_child]
- 枚举子节点的颜色(不与父节点冲突):
例题:
- LeetCode 968. 监控二叉树(最小覆盖问题)
- Codeforces 1223E. Paint the Tree(染色最大化价值)
通用技巧
- 遍历顺序:
- 通常用 DFS 后序遍历(先处理子树,再处理父节点)。
- 状态设计:
- 根据问题决定是否需要记录父节点状态(如
dp[u][c][parent_c])。
- 根据问题决定是否需要记录父节点状态(如
- 空间优化:
- 如果子节点状态可复用,可以用滚动数组(如
dp[2][...])。
- 如果子节点状态可复用,可以用滚动数组(如
- 边界处理:
- 叶子节点的初始化(如
dp[leaf][...] = base_value)。
- 叶子节点的初始化(如
模板代码(树型 DP 框架)
def tree_dp(root):
# 初始化 DP 表
dp = defaultdict(dict)
def dfs(u, parent):
# 初始化当前节点的状态
dp[u][...] = initial_value
for v in tree[u]:
if v == parent:
continue
dfs(v, u) # 递归处理子节点
# 合并子节点的状态
dp[u][...] = combine(dp[u][...], dp[v][...])
dfs(root, -1)
return dp[root][...]
典型例题
| 类型 | 例题 |
|---|---|
| 子树统计 | LeetCode 543. 二叉树的直径 |
| 树上背包 | LeetCode 337. 打家劫舍 III |
| 换根 DP | LeetCode 834. 树中距离之和 |
| 路径问题 | LeetCode 687. 最长同值路径 |
| 染色问题 | LeetCode 968. 监控二叉树 |
掌握这些类型后,可以解决大多数树型 DP 问题。建议从基础子树统计和树上背包问题入手,逐步扩展到换根 DP 和路径问题。
常见方法
在动态规划(DP)问题中,除了掌握各类问题的状态设计和转移方程外,通用优化技巧和解题策略同样至关重要。以下是结合前文所有讨论的 3 种常见方法及其适用场景的总结,帮助你在实战中快速识别问题并优化解法。
1. 优化枚举:减少无效状态或转移
核心思想:通过数学性质、贪心策略或问题约束,减少需要枚举的状态或决策,降低时间复杂度。
适用场景及技巧:
- 背包问题:
- 完全背包:正序枚举容量(
j从w[i]到W),避免重复计算的无效状态。 - 多重背包:二进制拆分将
s[i]次物品拆分为1, 2, 4,...的组合,转化为 0-1 背包,减少枚举次数。
- 完全背包:正序枚举容量(
- 区间 DP:
- 四边形不等式优化:将分割点
k的枚举范围从[i, j)缩小到[k_opt[i][j-1], k_opt[i+1][j]],时间复杂度从O(n^3)降至O(n^2)。 - 例题:石子合并问题。
- 四边形不等式优化:将分割点
- 树型 DP:
- 树上背包:逆序枚举容量(类似 0-1 背包),避免子树状态重复计算。
- 例题:洛谷 P2014 选课。
经典案例:
- LeetCode 322. 零钱兑换:完全背包中,正序枚举金额
amount可确保硬币无限使用。
2. 根据数据量猜解法:从输入规模反推算法
核心思想:通过题目给出的数据范围,快速判断可能的 DP 类型和优化方向。
常见数据范围与解法:
| 数据范围 (n) | 可能的 DP 类型 | 优化思路 |
|---|---|---|
n ≤ 20 | 状压 DP | 二进制状态压缩 |
n ≤ 100 | 区间 DP / 二维背包 | O(n^3) 或 O(n^2) 优化 |
n ≤ 1000 | 线性 DP / 树型 DP | 滚动数组优化空间 |
n ≤ 1e5 | 换根 DP / 贪心 + DP | 线性扫描或数学性质 |
实战技巧:
- 状压 DP:当
n ≤ 20时,状态数2^n约百万级,可直接枚举。- 例题:LeetCode 847. 访问所有节点的最短路径(
n=12)。
- 例题:LeetCode 847. 访问所有节点的最短路径(
- 数位 DP:当问题与数字位数相关(如
L,R ≤ 1e18),通常用数位 DP。- 例题:LeetCode 233. 数字 1 的个数。
3. 根据 DP 表反推具体方案:回溯或记录路径
核心思想:在 DP 求解最优值后,通过反向追踪状态转移路径,还原具体方案(如选哪些物品、如何分割区间等)。
常见方法:
- 记录转移来源:
- 在 DP 表中额外维护
from[i][j],记录状态(i,j)的最优转移来源。 - 例题:LeetCode 1143. 最长公共子序列,通过
from数组回溯 LCS 字符串。
- 在 DP 表中额外维护
- 贪心回溯:
- 根据 DP 值的单调性反向推导(如从终点倒推起点)。
- 例题:LeetCode 45. 跳跃游戏 II(记录每一步的最远跳跃位置)。
- 分步重构:
- 适用于背包问题,通过
dp[j]和dp[j-w[i]]的差值判断是否选了物品i。 - 例题:0-1 背包输出具体方案。
- 适用于背包问题,通过
代码模板(以 0-1 背包为例):
# 假设 dp[j] 表示容量 j 的最大价值,from[j] 记录是否选了物品 i
for i in range(n, 0, -1):
if j >= w[i] and dp[j] == dp[j - w[i]] + v[i]:
print(f"选物品 {i}")
j -= w[i]
综合应用场景
- 背包问题 + 输出方案:
- 先跑标准 DP,再逆序检查物品是否被选。
- 区间 DP + 构造分割点:
- 记录分割点
k,递归输出左右区间(如矩阵连乘问题)。
- 记录分割点
- 树型 DP + 换根求全局解:
- 第一次 DFS 计算子树信息,第二次 DFS 用父节点更新子节点(如 LeetCode 834. 树中距离之和)。
总结:方法选择与优先级
- 先看数据范围:确定 DP 类型(状压、区间、树型等)。
- 优化枚举:优先考虑数学性质或问题约束(如单调性、贪心剪枝)。
- 方案还原:若需输出路径,设计 DP 表时同步记录转移来源。
通过结合这些方法,可以高效解决大多数 DP 问题,并在竞赛或面试中快速定位优化方向。
5217

被折叠的 条评论
为什么被折叠?



