算法讲解088【必备】动态规划专题总结与预告

     b站链接:左程云的个人空间-左程云个人主页-哔哩哔哩视频

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(从 Ww[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 是当前组的物品。

​例题​​:

  • AcWing 9. 分组背包问题

​5. 二维费用背包​

​问题描述​​:

  • 背包有​​两种容量限制​​(如重量和体积),物品对应两种费用。

​解法​​:

  • ​状态扩展​​:dp[i][j][k] 表示前 i 个物品在两种容量 jk 时的最优解。
  • 优化为二维数组: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 过程中记录是否选择了当前物品。

​通用技巧​

  1. ​初始化​​:
    • 要求恰好装满:dp[0]=0,其他初始化为 -∞
    • 不要求装满:dp[0...W]=0
  2. ​遍历顺序​​:
    • 0-1 背包:逆序更新容量。
    • 完全背包:正序更新容量。
  3. ​问题转化​​:将非背包问题(如子集和、字符串匹配)转化为背包模型。

通过掌握这些类型和对应的状态转移方程,可以解决大多数背包 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 算法)。
  • 使用​​记忆化搜索​​减少重复计算。

​例题​​:

  • LeetCode 943. 最短超级串(类似 TSP)
  • 洛谷 P1171 售货员的难题

​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)
  • ​预处理​​:提前计算某些子集的性质(如是否合法)。

​例题​​:

  • LeetCode 698. 划分为 k 个相等的子集
  • LeetCode 1655. 分配重复整数

​4. 位运算优化 DP​

​问题描述​​:

  • 利用位运算(如 ANDORXOR)进行状态转移,常用于​​位操作相关的最优化问题​​。

​状态设计​​:

  • 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

​例题​​:

  • 洛谷 P5056 【模板】插头 DP
  • HDU 1693 Eat the Trees(多回路覆盖)

​通用技巧​

  1. ​状态压缩方式​​:
    • intlong long 的二进制位表示状态(mask & (1 << i) 检查第 i 位是否选中)。
  2. ​初始化与边界​​:
    • 初始状态通常为 dp[0][...] = 0dp[1 << start][...] = 0
  3. ​优化方法​​:
    • ​预处理合法状态​​(如 mask 不能有相邻的 1)。
    • ​滚动数组​​优化空间(如 dp[2][mask])。
  4. ​时间复杂度​​:
    • 通常为 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、数位递增等)。

​解法​​:

  1. ​状态设计​​:
    • dp[pos][state][limit]
      • pos:当前处理到数字的第 pos 位(从高位到低位)。
      • state:记录前几位的影响(如前缀和、前导零等)。
      • limit:是否受数字上限约束(如 R=123,前两位是 12 时第三位不能超过 3)。
  2. ​转移方程​​:
    • 枚举当前位的数字 d09,受 limit 限制)。
    • 根据 state 更新状态(如数位和、是否出现某数字)。
  3. ​记忆化搜索​​:
    • 递归实现,记忆化 limit=false 的状态(避免重复计算)。

​例题​​:

  • LeetCode 233. 数字 1 的个数
  • LeetCode 1012. 至少有 1 位重复的数字

​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. 数字转换问题(二进制、特殊进制)​

​问题描述​​:

  • 处理非十进制数字(如二进制、三进制)的统计问题。

​解法​​:

  • ​调整数位枚举范围​​(如二进制枚举 01)。
  • ​状态设计​​与十进制类似,但需注意进制转换。

​例题​​:

  • LeetCode 600. 不含连续 1 的非负整数(二进制数位 DP)
  • LeetCode 902. 最大为 N 的数字组合(任意进制)

​通用技巧​

  1. ​问题转化​​:
    • [L, R] 的问题转化为 [0, R] - [0, L-1]
  2. ​状态设计原则​​:
    • 必须包含​​当前位 pos​ 和​​约束状态 limit​。
    • 根据问题添加额外状态(如 summod、前导零 lead)。
  3. ​记忆化优化​​:
    • 仅记忆化 limit=false 的状态(limit=true 的状态只会计算一次)。
  4. ​边界处理​​:
    • 注意 L=0R=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] 的最优解或方案数。
  • ​转移方程​​:
    • 枚举分割点 ki ≤ 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. 切棍子的最小成本(环形切割问题)

​通用技巧​

  1. ​遍历顺序​​:
    • 区间 DP 通常按​​区间长度从小到大​​枚举(先算小区间,再算大区间)。
  2. ​初始化​​:
    • 单个元素的区间 dp[i][i] 通常有初始值(如回文长度为 1,合并代价为 0)。
  3. ​时间复杂度优化​​:
    • 四边形不等式优化(将 O(n^3) 优化到 O(n^2))。
  4. ​空间优化​​:
    • 滚动数组(如 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(二次扫描)​

​问题描述​​:

  • 对每个节点作为根时,求整棵树的性质(如节点到其他节点的距离和、最大值等)。

​解法​​:

  1. ​第一次 DFS​​:计算以某个根(如 u=0)为基准的子树信息(如 dp[u])。
  2. ​第二次 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)  # 拐弯路径

​例题​​:

  • LeetCode 687. 最长同值路径
  • LeetCode 1372. 二叉树中的最长交错路径

​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(染色最大化价值)

​通用技巧​

  1. ​遍历顺序​​:
    • 通常用 ​​DFS 后序遍历​​(先处理子树,再处理父节点)。
  2. ​状态设计​​:
    • 根据问题决定是否需要记录父节点状态(如 dp[u][c][parent_c])。
  3. ​空间优化​​:
    • 如果子节点状态可复用,可以用滚动数组(如 dp[2][...])。
  4. ​边界处理​​:
    • 叶子节点的初始化(如 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. 优化枚举:减少无效状态或转移​

​核心思想​​:通过数学性质、贪心策略或问题约束,减少需要枚举的状态或决策,降低时间复杂度。

​适用场景及技巧​​:
  • ​背包问题​​:
    • ​完全背包​​:正序枚举容量(jw[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)。
  • ​数位 DP​​:当问题与数字位数相关(如 L,R ≤ 1e18),通常用数位 DP。
    • ​例题​​:LeetCode 233. 数字 1 的个数。

​3. 根据 DP 表反推具体方案:回溯或记录路径​

​核心思想​​:在 DP 求解最优值后,通过反向追踪状态转移路径,还原具体方案(如选哪些物品、如何分割区间等)。

​常见方法​​:
  1. ​记录转移来源​​:
    • 在 DP 表中额外维护 from[i][j],记录状态 (i,j) 的最优转移来源。
    • ​例题​​:LeetCode 1143. 最长公共子序列,通过 from 数组回溯 LCS 字符串。
  2. ​贪心回溯​​:
    • 根据 DP 值的单调性反向推导(如从终点倒推起点)。
    • ​例题​​:LeetCode 45. 跳跃游戏 II(记录每一步的最远跳跃位置)。
  3. ​分步重构​​:
    • 适用于背包问题,通过 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]

​综合应用场景​

  1. ​背包问题 + 输出方案​​:
    • 先跑标准 DP,再逆序检查物品是否被选。
  2. ​区间 DP + 构造分割点​​:
    • 记录分割点 k,递归输出左右区间(如矩阵连乘问题)。
  3. ​树型 DP + 换根求全局解​​:
    • 第一次 DFS 计算子树信息,第二次 DFS 用父节点更新子节点(如 LeetCode 834. 树中距离之和)。

​总结:方法选择与优先级​

  1. ​先看数据范围​​:确定 DP 类型(状压、区间、树型等)。
  2. ​优化枚举​​:优先考虑数学性质或问题约束(如单调性、贪心剪枝)。
  3. ​方案还原​​:若需输出路径,设计 DP 表时同步记录转移来源。

通过结合这些方法,可以高效解决大多数 DP 问题,并在竞赛或面试中快速定位优化方向。

你可能感兴趣的:(左神课程学习,动态规划,算法)