当前位置: 首页 > news >正文

状态压缩 DP 与树形 DP:从空间优化到树状结构的动态规划

状态压缩 DP 与树形 DP:从空间优化到树状结构的动态规划

一、DP 的"空间焦虑"与"树形困境":两种进阶场景的挑战

动态规划的基础题型(背包、子序列、路径)大多可以用二维数组解决,状态转移清晰直观。但两类进阶场景会打破这种舒适感:一是状态维度爆炸——旅行商问题(TSP)的朴素 DP 需要 O(n²·2ⁿ) 空间,n=20 时就需要约 200MB;二是树形依赖——树上的 DP 没有天然的线性顺序,子树之间的状态传递需要后序遍历,且经常面临"选/不选当前节点"的决策。

状态压缩 DP 用二进制位表示集合状态,将指数级的集合枚举压缩为整数运算;树形 DP 利用树的后序遍历顺序,自底向上传递子树状态。两者虽然场景不同,但核心思想一致:找到状态的紧凑表示方式,让转移方程可计算。

二、状态压缩 DP 与树形 DP 的原理

graph TB subgraph 状态压缩DP A[集合 S 用二进制表示<br/>S=1011 表示选了0,1,3号城市] A --> B[状态转移<br/>dp S,j = min dp S-2^j, i + dist i,j] B --> C[遍历顺序<br/>按 S 的二进制中1的个数递增] end subgraph 树形DP D[后序遍历<br/>先处理子树] --> E[状态定义<br/>dp u,0 = 不选u<br/>dp u,1 = 选u] E --> F[转移方程<br/>dp u,0 += max dp c,0, dp c,1<br/>dp u,1 += dp c,0] end

状态压缩 DP 的关键洞察是:n 个元素的子集可以用 n 位二进制数表示。枚举所有子集等价于枚举 0 到 2ⁿ-1 的整数。状态转移时,通过位运算(检查某位是否为 1、设置/清除某位)高效操作集合。

树形 DP 的关键洞察是:树的后序遍历保证了"处理当前节点时,所有子树的状态已经计算完毕"。因此,树形 DP 的转移方程天然是自底向上的,不需要额外的拓扑排序。

三、代码实现

3.1 状态压缩 DP:旅行商问题

from typing import List def tsp(dist: List[List[int]]) -> int: """ 旅行商问题:求经过所有城市恰好一次并回到起点的最短路径。 使用状态压缩 DP,时间 O(n²·2ⁿ),空间 O(n·2ⁿ)。 """ n = len(dist) if n <= 1: return 0 INF = float('inf') # dp[S][j] 表示已访问集合 S,当前在城市 j 的最短路径 # S 用二进制表示,第 i 位为 1 表示城市 i 已访问 full_mask = (1 << n) - 1 dp = [[INF] * n for _ in range(1 << n)] # 起点为城市 0,初始状态只访问了城市 0 dp[1][0] = 0 # 按 S 中 1 的个数递增遍历 for s in range(1, 1 << n): for u in range(n): if dp[s][u] == INF: continue if not (s & (1 << u)): continue # u 不在集合 S 中,跳过 # 尝试从 u 走到未访问的城市 v for v in range(n): if s & (1 << v): continue # v 已访问,跳过 new_state = s | (1 << v) new_dist = dp[s][u] + dist[u][v] if new_dist < dp[new_state][v]: dp[new_state][v] = new_dist # 所有城市都访问后,回到起点 0 result = INF for u in range(1, n): if dp[full_mask][u] + dist[u][0] < result: result = dp[full_mask][u] + dist[u][0] return result

3.2 树形 DP:树的最大独立集

from collections import defaultdict from typing import Dict, List, Tuple class TreeDP: """树形 DP:求解树上的最大独立集""" def __init__(self, n: int): self.n = n self.adj = defaultdict(list) def add_edge(self, u: int, v: int) -> None: """添加无向边""" self.adj[u].append(v) self.adj[v].append(u) def max_independent_set(self) -> Tuple[int, List[int]]: """ 求树的最大独立集。 dp[u][0] = 不选节点 u 时,以 u 为根的子树的最大独立集 dp[u][1] = 选节点 u 时,以 u 为根的子树的最大独立集 """ # dp[u][0]: 不选 u,子节点可选可不选 # dp[u][1]: 选 u,子节点必须不选 dp = [[0, 0] for _ in range(self.n)] visited = [False] * self.n def dfs(u: int) -> None: visited[u] = True dp[u][0] = 0 # 不选 u dp[u][1] = 1 # 选 u,权重为 1 for v in self.adj[u]: if visited[v]: continue # 跳过父节点 dfs(v) # 不选 u 时,子节点选或不选取较大值 dp[u][0] += max(dp[v][0], dp[v][1]) # 选 u 时,子节点必须不选 dp[u][1] += dp[v][0] dfs(0) # 回溯求具体方案 selected = [] max_size = max(dp[0][0], dp[0][1]) def backtrack(u: int, parent_selected: bool) -> None: if parent_selected: # 父节点已选,当前节点不能选 for v in self.adj[u]: if not visited[v] or v in selected: continue backtrack(v, False) else: # 父节点未选,当前节点可选可不选 select_u = dp[u][1] >= dp[u][0] if select_u: selected.append(u) for v in self.adj[u]: if v in selected: continue backtrack(v, select_u) # 简化:重新标记 visited 用于回溯 visited = [False] * self.n selected = [] self._backtrack_simple(0, -1, dp, selected) return max_size, selected def _backtrack_simple( self, u: int, parent: int, dp: List[List[int]], selected: List[int] ) -> None: """回溯求最大独立集的具体节点""" # 判断是否选择当前节点 if parent == -1 or u not in [ v for v in self.adj[parent] ]: # 根节点或非子节点,独立判断 pass select_u = (parent == -1 and dp[u][1] >= dp[u][0]) or \ (parent != -1 and dp[u][1] > dp[u][0]) # 如果父节点未选,且选 u 更优,则选 u if parent != -1: # 检查父节点是否被选 parent_selected = parent in selected if not parent_selected and dp[u][1] > dp[u][0]: selected.append(u) select_u = True else: select_u = False else: if dp[u][1] >= dp[u][0]: selected.append(u) select_u = True for v in self.adj[u]: if v != parent: self._backtrack_simple(v, u, dp, selected)

3.3 状态压缩 DP:位运算技巧

class BitTricks: """状态压缩 DP 中常用的位运算技巧""" @staticmethod def count_bits(x: int) -> int: """统计二进制中 1 的个数(集合大小)""" count = 0 while x: x &= x - 1 # 清除最低位的 1 count += 1 return count @staticmethod def enumerate_subsets(s: int): """枚举集合 S 的所有非空子集""" sub = s while sub: yield sub sub = (sub - 1) & s @staticmethod def lowest_bit(x: int) -> int: """获取最低位的 1(提取最小元素)""" return x & (-x) @staticmethod def is_subset(s: int, t: int) -> bool: """判断 S 是否为 T 的子集""" return (s & t) == s @staticmethod def iterate_by_popcount(n: int): """按 1 的个数递增枚举所有子集""" from collections import defaultdict buckets = defaultdict(list) for s in range(1 << n): cnt = bin(s).count('1') buckets[cnt].append(s) for cnt in sorted(buckets.keys()): yield from buckets[cnt]

四、状态压缩 DP 与树形 DP 的 Trade-offs 分析

状态压缩 DP 的规模限制:n=20 时,2²⁰ ≈ 100 万个状态,内存和时间都可接受;n=25 时,2²⁵ ≈ 3300 万,开始吃力;n=30 时,2³⁰ ≈ 10 亿,不可行。状态压缩 DP 的适用边界是 n ≤ 20-25。超过这个范围,需要考虑分支限界、近似算法或问题特化。

树形 DP 的树结构依赖:树形 DP 要求输入必须是树(无环连通图)。如果输入是森林(多棵树),需要对每棵树分别求解再合并;如果输入是 DAG,需要拓扑排序后再 DP;如果输入有环,需要先破环(如基环树 DP)。

空间优化:状态压缩 DP 的空间为 O(n·2ⁿ),当 n 较大时内存紧张。可以按层滚动(只保留当前层和上一层),将空间降为 O(2ⁿ)。树形 DP 的空间为 O(n),通常不是瓶颈。

回溯求方案:DP 求最优值容易,但回溯求具体方案需要额外记录转移路径。状态压缩 DP 的回溯需要 O(n·2ⁿ) 的额外空间;树形 DP 的回溯可以在 DFS 中同步完成,无需额外空间。

五、总结

状态压缩 DP 和树形 DP 是动态规划的两类进阶范式。状态压缩 DP 用二进制位表示集合,将指数级枚举转化为可计算的整数运算,适用于 n ≤ 20 的组合优化问题;树形 DP 利用后序遍历的自底向上特性,在树结构上高效传递状态,适用于树上的决策问题。

落地建议:遇到"从 n 个元素中选子集"的问题,优先考虑状态压缩 DP;遇到"树上的选/不选决策"问题,优先考虑树形 DP。两者都需要先定义清晰的状态表示和转移方程,再考虑优化。状态压缩 DP 注意规模限制,树形 DP 注意输入是否为树。

http://www.jsqmd.com/news/988693/

相关文章:

  • java feign调用第三方服务出现序列化错误的排查过程
  • 【手把手教学】:OpenClaw 解压安装与运行全流程(包含安装包)
  • 告别Token烧钱焦虑!「秒云Tokens管家」智能预警,筑牢AI成本防线
  • Spring Boot 配置文件敏感信息加密(Jasypt 企业级完整方案)
  • 计算机毕业设计之django基于大数据的旅游景区推荐系统
  • 2026年滑块图形验证码服务商推荐:安全与体验兼得的选择
  • [智能体-333]:LangGraph代码示例,详细注解:基础线性图、条件分支、循环、人在回路
  • 皮皮出海:助力国内企业出海增长
  • 卫生间漏水维修全攻略:上海尤卉教你快速排查与解决漏水问题
  • 3DS游戏文件转换解决方案:从CCI到CIA的高效处理流程
  • 英雄联盟Akari助手:3个核心功能让你游戏效率提升500%的免费开源工具
  • 百度网盘Mac版功能增强方案:技术实现与部署指南
  • 2026年 广东/东莞铁艺装饰花件厂家推荐榜:失蜡铸造花件、铁艺装饰花件源头工厂专业实力与精工匠心之选 - 品牌发掘
  • 企业真人数字人制作怎么选?2026低成本高精度制作平台性价比对比
  • 第六节:Slash Commands斜杠命令——AI 的快捷指令
  • 孔夫子旧书网批量抓取工具:自动登录+商品信息提取+Excel导出
  • 海外商标注册后怎么管?出海企业不能让商标停在代理邮件里 - 客啦啦视界
  • 执行计划深度解析:从 type 到 Extra,榨干 EXPLAIN 的价值
  • AD生成图形交互式bom表的方法
  • 北京配眼镜功能性镜片怎么选,五类场景逐一对照 - 配眼镜新资讯
  • 网盘直链下载助手终极指南:免费获取八大网盘真实下载地址
  • 五指毛桃赤小豆膏:从古籍配伍到现代轻养生的配方逻辑
  • 测评|苏州外贸工厂做GEO应该怎么选服务商?靠谱GEO服务商推荐? - 极义GEO
  • i.MX 8ULP硬件设计:电源时序与未用接口处理实战指南
  • 终极Qobuz无损音乐下载器:专业级音乐库构建完整指南
  • 数据的加密与解密(23:22)
  • 完整指南:在macOS上轻松运行Windows程序的终极解决方案
  • 压敏电阻 Cp 参数怎么看?电源端与信号端应用差异解析
  • DBHub:一款免费开源的数据库MCP服务器
  • Dify日志与标注时间显示问题