☰
树形动态规划:高效计算节点间距离和
2026/11/6 10:28:04 网站建设 项目流程
## 1. 问题背景与核心挑战 最近在刷算法题时遇到一道很有意思的树形DP问题——"Tree Distances II"。题目给定一棵包含n个节点的树,要求计算每个节点到其他所有节点的距离之和。初看觉得直接暴力DFS就能解决,但仔细一想,当n达到1e5量级时,O(n²)的复杂度显然会TLE。这让我开始思考如何用O(n)或O(nlogn)的复杂度优雅解决这个问题。 这类树形动态规划问题在实际开发中其实很常见。比如在社交网络分析中,我们可能需要计算某个用户到其他所有用户的"关系距离";在分布式系统中,需要优化节点间的通信成本;甚至在前端渲染大型DOM树时,也需要类似思路来优化遍历性能。理解这类算法对提升实际问题解决能力很有帮助。 ## 2. 暴力解法与复杂度分析 ### 2.1 朴素DFS实现 最直观的做法是对每个节点执行一次DFS/BFS遍历: ```python def brute_force(tree): n = len(tree) res = [0]*n for u in range(n): visited = [False]*n stack = [(u,0)] while stack: node, dist = stack.pop() visited[node] = True res[u] += dist for v in tree[node]: if not visited[v]: stack.append((v, dist+1)) return res

这个实现的时间复杂度是O(n²),当n=1e5时需要1e10次操作,显然不可行。

2.2 树形结构的特殊性质

观察到树是无向无环连通图,具有以下关键性质:

  1. 任意两点间有且只有一条简单路径
  2. 移除任意边会将树分成两个连通分量
  3. 可以选取任意节点作为根节点转化为有根树

这些性质提示我们可以利用动态规划来避免重复计算。

3. 优化解法:二次扫描动态规划

3.1 第一次DFS:后序遍历统计子树信息

首先任选一个根节点(通常选0号节点),定义:

  • count[u]: 以u为根的子树节点数
  • res[u]: u到其子树所有节点的距离和

第一次DFS从根节点开始后序遍历:

def dfs1(u, parent): count[u] = 1 res[u] = 0 for v in tree[u]: if v != parent: dfs1(v, u) count[u] += count[v] res[u] += res[v] + count[v]

关键点解释:

  • count[v]表示子树v的大小
  • res[v]是v到其子树节点的距离和
  • 从v到u的每条边会被子树v的所有节点各走一次,所以增加count[v]

3.2 第二次DFS:前序遍历推导全局解

定义ans[u]为u到所有节点的距离和。对于根节点root,显然ans[root]=res[root]。

对于其他节点u,其父节点为p,有状态转移方程:

ans[u] = ans[p] - count[u] + (n - count[u])

推导过程:

  1. 从p切换到u时,u的子树节点距离都-1(共count[u]个)
  2. 其他节点距离都+1(共n-count[u]个)

实现代码:

def dfs2(u, parent): for v in tree[u]: if v != parent: ans[v] = ans[u] - count[v] + (n - count[v]) dfs2(v, u)

3.3 完整算法实现

def tree_distances(tree): n = len(tree) count = [0]*n res = [0]*n ans = [0]*n def dfs1(u, parent): count[u] = 1 for v in tree[u]: if v != parent: dfs1(v, u) count[u] += count[v] res[u] += res[v] + count[v] def dfs2(u, parent): for v in tree[u]: if v != parent: ans[v] = ans[u] - count[v] + (n - count[v]) dfs2(v, u) dfs1(0, -1) ans[0] = res[0] dfs2(0, -1) return ans

4. 复杂度分析与正确性验证

4.1 时间复杂度

  • 两次DFS遍历,每个节点访问一次
  • 每个边被访问两次(无向边)
  • 总时间复杂度:O(n)

4.2 空间复杂度

  • count/res/ans数组:O(n)
  • 递归栈最坏情况O(n)(退化成链)
  • 总空间复杂度:O(n)

4.3 边界情况测试

  1. 单节点树:返回[0]
  2. 链状树:如0-1-2-3,应返回[3,2,2,3]
  3. 星型树:中心节点距离和最小
  4. 完全二叉树:验证对称性

5. 实际应用与变种问题

5.1 应用场景案例

  1. 网络监控中心选址:选择到所有节点距离和最小的点部署监控服务
  2. 电商仓储优化:计算仓库到所有配送点的总距离
  3. 社交网络分析:找出关系网中最"中心"的人物

5.2 常见变种题型

  1. 加权树:边带有权重,计算加权距离和
  2. 带有点权的树:考虑节点权重的影响
  3. 动态树:支持边的增删操作
  4. 找出距离和最小的k个节点

6. 调试技巧与常见错误

6.1 典型Debug场景

  1. 忘记处理父节点导致循环调用:
# 错误写法 for v in tree[u]: dfs1(v, u) # 缺少parent判断 # 正确写法 for v in tree[u]: if v != parent: dfs1(v, u)
  1. 状态转移公式错误:
  • 混淆count和res的含义
  • 忘记n-count[v]的部分

6.2 验证技巧

  1. 小规模测试:手动计算n=3,4的情况
  2. 对称性验证:对称树的结果应对称
  3. 极值验证:链状树端点值应为最大

关键提示:在竞赛中遇到类似问题,建议先用小样例验证状态转移方程的正确性,再处理大规模数据。我曾经因为直接写大数组导致RE浪费半小时。

7. 算法优化与扩展思路

7.1 空间优化版本

对于极端大n的情况(如1e6),可以省去res数组:

def dfs1(u, parent): count[u] = 1 total = 0 for v in tree[u]: if v != parent: total += dfs1(v, u) count[u] += count[v] ans[u] = total return total + count[u]

7.2 非递归实现

使用栈模拟递归,避免爆栈:

def dfs1_iterative(root): stack = [(root, -1, False)] while stack: u, parent, visited = stack.pop() if not visited: stack.append((u, parent, True)) for v in reversed(tree[u]): if v != parent: stack.append((v, u, False)) else: count[u] = 1 res[u] = 0 for v in tree[u]: if v != parent: count[u] += count[v] res[u] += res[v] + count[v]

7.3 多叉树处理

当子树数量不定时,需要动态处理子节点:

for children in tree[u]: for v in children: if v != parent: ...

这个树形DP问题的核心在于发现父子节点间的递推关系。通过两次扫描,我们巧妙地将O(n²)问题转化为O(n)解法。掌握这种思路后,可以解决LeetCode 310、834等一系列相似问题。在实际编码时,建议先画出树结构,手动推导小样例,再抽象出状态转移方程。记住:树形DP的要点是合理设计状态,利用子树信息推导全局解。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询