b站链接:左程云的个人空间-左程云个人主页-哔哩哔哩视频
GitHub链接:algorithmzuo (左程云) · GitHub
1.尝试,进行逻辑分析,分类讨论,观察规律,从简单到复杂,正难求反,利用答案的集合性质等等,观察数据量。
2.写出递归
3.挂缓存,改成记忆化搜索
4.根据记忆化搜索写出严格位置依赖版本的dp
5.画图,举例子,建立空间感,构建空间压缩版本。
背包动态规划(DP)是动态规划中的经典问题,其核心在于在有限容量的背包中选择物品以达到最优解(如最大价值、最小成本等)。以下是常见的背包问题类型及其解法总结:
问题描述:
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]
)。例题:
问题描述:
解法:
dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i])
(注意 i
不变,表示可重复选)。j
(从 w[i]
到 W
)。例题:
问题描述:
i
最多选 s[i]
次。解法:
s[i]
拆成 1, 2, 4, ..., 2^k, s[i]-2^k
的组合,转化为 0-1 背包。O(NW)
(较复杂)。例题:
问题描述:
解法:
dp[j] = max(dp[j], dp[j-w[k]] + v[k])
,其中 k
是当前组的物品。例题:
问题描述:
解法:
dp[i][j][k]
表示前 i
个物品在两种容量 j
和 k
时的最优解。dp[j][k] = max(dp[j][k], dp[j-w1[i]][k-w2[i]] + v[i])
。例题:
问题描述:
解法:
dp[j]
表示容量 j
的方案数。dp[j] += dp[j-w[i]]
(初始 dp[0]=1
)。例题:
问题描述:
解法:
dp[N][W]
倒推物品选择。dp[0]=0
,其他初始化为 -∞
。dp[0...W]=0
。通过掌握这些类型和对应的状态转移方程,可以解决大多数背包 DP 问题。建议从 0-1 背包和完全背包入手,逐步扩展到其他变种。
状压动态规划(状态压缩 DP)通常用于处理状态包含多个维度且每个维度是二元选择的问题(如“选/不选”、“存在/不存在”)。其核心是通过二进制数压缩状态,从而高效地表示和转移状态。以下是常见的状压 DP 类型及解法总结:
问题描述:
状态设计:
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]
(所有点都访问过,并回到起点)。优化:
例题:
问题描述:
状态设计:
dp[i][mask]
:表示处理到第 i
行时,当前行的状态为 mask
(二进制位表示是否放置)。prev_mask
,检查是否冲突(如列冲突、对角线冲突)。dp[i][mask] += dp[i-1][prev_mask]
(合法转移)。例题:
问题描述:
状态设计:
dp[mask]
:表示当前子集 mask
的最优解(如最小操作数、最大价值等)。mask
的所有子集 submask
,更新 dp[mask] = min(dp[mask], dp[submask] + cost)
。优化:
for (int submask = mask; submask; submask = (submask - 1) & mask)
例题:
问题描述:
AND
、OR
、XOR
)进行状态转移,常用于位操作相关的最优化问题。状态设计:
dp[mask]
:表示当前位状态 mask
的最优解。dp[mask | new_bits] = min(dp[mask] + cost)
。例题:
问题描述:
状态设计:
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 问题 和 棋盘放置问题 入手,熟悉状态压缩的基本思路。
数位动态规划(Digit DP)用于解决与数字各位数字相关的计数或最优化问题,通常涉及数字的位数限制、数字性质(如回文、数位和)、区间统计等。以下是常见的数位 DP 类型及解法总结:
问题描述:
[L, R]
,统计满足某种条件的数字个数(如不含 4
、数位递增等)。解法:
dp[pos][state][limit]
:
pos
:当前处理到数字的第 pos
位(从高位到低位)。state
:记录前几位的影响(如前缀和、前导零等)。limit
:是否受数字上限约束(如 R=123
,前两位是 12
时第三位不能超过 3
)。d
(0
到 9
,受 limit
限制)。state
更新状态(如数位和、是否出现某数字)。limit=false
的状态(避免重复计算)。例题:
问题描述:
[L, R]
内数位和满足条件的数字(如和等于 K
、是 K
的倍数等)。解法:
state
中记录当前数位和 sum
。sum
超过目标或无法达到目标,提前终止。例题:
问题描述:
解法:
例题:
问题描述:
K
等于特定值的数字个数(如 %K=0
)。解法:
state
中记录当前数字模 K
的值 mod
。new_mod = (mod * 10 + d) % K
,更新状态。例题:
问题描述:
解法:
0
或 1
)。例题:
[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
的数字)入手,逐步扩展到复杂状态设计(如模数、数位和)。
区间动态规划(Interval DP)是一种用于解决区间划分、合并、最优解问题的动态规划方法,其核心思想是通过枚举区间的分割点,逐步求解更大区间的最优解。以下是常见的区间 DP 类型及解法总结:
问题描述:
解法:
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))
。例题:
问题描述:
解法:
dp[i][j]
:表示合并区间 [i, j]
的最小代价。dp[i][j] = min(dp[i][k] + dp[k+1][j] + sum(i, j))
,其中 sum(i, j)
是区间 [i, j]
的权重和。例题:
问题描述:
K
),求最小划分数或最大收益。解法:
dp[i]
:表示前 i
个元素的最优解(通常一维即可)。dp[i] = min(dp[j] + cost(j+1, i))
,其中 j < i
且 [j+1, i]
满足条件。例题:
问题描述:
解法:
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])
。例题:
问题描述:
解法:
nums + nums
),然后对 2n
长度的序列做区间 DP。i
,取 dp[i][i+n-1]
的最优解。例题:
dp[i][i]
通常有初始值(如回文长度为 1,合并代价为 0)。O(n^3)
优化到 O(n^2)
)。dp[2][n]
)或对角线遍历(减少空间占用)。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 的基本思路。
树型动态规划(Tree DP)是一种基于树结构的动态规划方法,常用于处理树上的最优解、计数、路径统计等问题。以下是常见的树型 DP 类型及解法总结:
问题描述:
解法:
dp[u]
:表示以节点 u
为根的子树的最优解或统计值。dp[u] = combine(dp[v1], dp[v2], ...)
。例题:
问题描述:
解法:
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])
例题:
问题描述:
解法:
u=0
)为基准的子树信息(如 dp[u]
)。dp[v] = dp[u] - contribution(v) + new_contribution(v)
。例题:
问题描述:
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) # 拐弯路径
例题:
问题描述:
解法:
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]
例题:
dp[u][c][parent_c]
)。dp[2][...]
)。dp[leaf][...] = base_value
)。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 种常见方法及其适用场景的总结,帮助你在实战中快速识别问题并优化解法。
核心思想:通过数学性质、贪心策略或问题约束,减少需要枚举的状态或决策,降低时间复杂度。
j
从 w[i]
到 W
),避免重复计算的无效状态。s[i]
次物品拆分为 1, 2, 4,...
的组合,转化为 0-1 背包,减少枚举次数。k
的枚举范围从 [i, j)
缩小到 [k_opt[i][j-1], k_opt[i+1][j]]
,时间复杂度从 O(n^3)
降至 O(n^2)
。amount
可确保硬币无限使用。核心思想:通过题目给出的数据范围,快速判断可能的 DP 类型和优化方向。
数据范围 (n) | 可能的 DP 类型 | 优化思路 |
---|---|---|
n ≤ 20 |
状压 DP | 二进制状态压缩 |
n ≤ 100 |
区间 DP / 二维背包 | O(n^3) 或 O(n^2) 优化 |
n ≤ 1000 |
线性 DP / 树型 DP | 滚动数组优化空间 |
n ≤ 1e5 |
换根 DP / 贪心 + DP | 线性扫描或数学性质 |
n ≤ 20
时,状态数 2^n
约百万级,可直接枚举。
n=12
)。L,R ≤ 1e18
),通常用数位 DP。
核心思想:在 DP 求解最优值后,通过反向追踪状态转移路径,还原具体方案(如选哪些物品、如何分割区间等)。
from[i][j]
,记录状态 (i,j)
的最优转移来源。from
数组回溯 LCS 字符串。dp[j]
和 dp[j-w[i]]
的差值判断是否选了物品 i
。# 假设 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]
k
,递归输出左右区间(如矩阵连乘问题)。通过结合这些方法,可以高效解决大多数 DP 问题,并在竞赛或面试中快速定位优化方向。