好的,我会从图论算法中随机挑选一个你从未听过的题目,为你细致讲解。今天我为你选择的题目是:
最小顶点覆盖问题的基于树形动态规划的精确算法(树上的精确解法)
题目描述
给定一棵树(一个无环、连通的无向图),以及每个顶点上的一个正整数权重。我们的目标是找到这棵树的一个最小顶点覆盖(Minimum Vertex Cover, MVC),并使得这个顶点覆盖中所有顶点的权重之和最小。
概念定义:
- 顶点覆盖:一个顶点集合,使得树中的每一条边都至少有一个端点(顶点)在这个集合里。
- 最小顶点覆盖:在所有可能的顶点覆盖中,顶点权重之和最小的那一个。
输入:
- 一棵有
n个顶点的树,通常以节点编号1到n表示。 - 每个顶点
i有一个权重w[i]。 - 树的
n-1条边。
输出:
- 最小顶点覆盖的最小总权重。
这是一个经典的树形动态规划(Tree DP)问题,可以高效地通过深度优先搜索(DFS)在 O(n) 时间内精确解决。
解题过程
这个问题的核心思想是:对于树上的每一个节点,我们考虑其状态,并利用其子树的信息来递推其父节点的最优解。
步骤 1:问题分析与状态设计
树是天然的递归结构。我们可以任选一个节点作为根节点(例如节点 1),将无根树转换为有根树。一旦有了根,每个节点(除了根)都有一个父节点和若干子节点。
对于以节点 u 为根的子树,我们定义两个状态:
dp[u][0]:表示在以u为根的子树中,不选择节点u进入顶点覆盖时,该子树内满足顶点覆盖条件所需的最小权重和。dp[u][1]:表示在以u为根的子树中,选择节点u进入顶点覆盖时,该子树内满足顶点覆盖条件所需的最小权重和。
我们的最终目标是求出 min(dp[root][0], dp[root][1])。
步骤 2:推导状态转移方程
状态转移依赖于节点 u 与其子节点 v 之间的关系。让我们分别考虑 dp[u][0] 和 dp[u][1] 如何从其子节点的 dp 值计算得来。
-
情况一:
dp[u][0](不选u进入覆盖)- 如果
u不被选入覆盖,那么为了覆盖连接u与其子节点v的那条边(u, v),我们必须选择它的子节点v进入覆盖! - 因此,对于
u的每一个子节点v,我们只能采用v被选中的状态,即dp[v][1]。 - 状态转移方程为:
dp[u][0] = Σ (dp[v][1]),对所有子节点v求和。
- 如果
-
情况二:
dp[u][1](选择u进入覆盖)- 如果
u被选入覆盖,那么u与所有子节点之间的边都已经被u覆盖了。因此,对于子节点v,我们既可以选择它,也可以不选择它,只需取两者中代价更小的方案即可。 - 由于我们选择了
u,需要加上u本身的权重w[u]。 - 状态转移方程为:
dp[u][1] = w[u] + Σ ( min( dp[v][0], dp[v][1] ) ),对所有子节点v求和。
- 如果
边界条件(叶子节点):
- 对于一个叶子节点
u(没有子节点):dp[u][0] = 0(不选它,但没有边需要覆盖,合法)dp[u][1] = w[u](选它,代价就是其自身权重)
步骤 3:计算顺序与算法实现
由于 dp[u] 依赖于其所有子节点 v 的 dp 值,我们必须先计算子节点的值,再计算父节点。这自然对应着对树进行后序遍历(DFS)。
算法框架:
- 将给定的树构建成一个邻接表
adj来表示。 - 任选一个节点(如 1 号节点)作为根,进行 DFS。
- 在 DFS 过程中,当访问完节点
u的所有子节点后,根据上述转移方程计算dp[u][0]和dp[u][1]。 - DFS 返回到根节点后,最终答案就是
min(dp[root][0], dp[root][1])。
伪代码:
// 假设树的邻接表为 adj,权重数组为 w
// dp[u][0/1] 初始化为0
// visited 数组用于DFS
function DFS(u, parent):
dp[u][1] = w[u] // 初始化选中的代价
dp[u][0] = 0 // 初始化不选的代价
for each v in adj[u]:
if v != parent: // 避免走回父节点
DFS(v, u) // 递归处理子节点
// 后序遍历,子节点v的dp值已计算好
dp[u][1] += min(dp[v][0], dp[v][1])
dp[u][0] += dp[v][1]
// 主程序
选择根节点 root = 1
DFS(root, -1)
答案 ans = min(dp[root][0], dp[root][1])
输出 ans
步骤 4:一个具体的例子
假设有一棵树如下(括号内为顶点权重):
1(5)
/ \
2(2) 3(10)
/ \
4(1) 5(3)
我们以节点 1 为根进行计算。
计算过程(自底向上):
-
叶子节点 4 和 5:
dp[4][0]=0,dp[4][1]=1dp[5][0]=0,dp[5][1]=3
-
节点 2:
- 子节点是 4 和 5。
dp[2][1]=w[2] + min(dp[4][0], dp[4][1]) + min(dp[5][0], dp[5][1])
=2 + min(0, 1) + min(0, 3)=2 + 0 + 0 = 2dp[2][0]=dp[4][1] + dp[5][1]=1 + 3 = 4
-
节点 3(叶子):
dp[3][0]=0,dp[3][1]=10
-
根节点 1:
- 子节点是 2 和 3。
dp[1][1]=w[1] + min(dp[2][0], dp[2][1]) + min(dp[3][0], dp[3][1])
=5 + min(4, 2) + min(0, 10)=5 + 2 + 0 = 7dp[1][0]=dp[2][1] + dp[3][1]=2 + 10 = 12
最终结果:min(dp[1][0], dp[1][1]) = min(12, 7) = 7。
对应的最小顶点覆盖:选择节点 1 和节点 2(总权重 5+2=7)。可以验证,所有边 (1,2), (1,3), (2,4), (2,5) 都被覆盖了。
步骤 5:算法复杂度分析
- 时间复杂度:
O(n)。我们对树进行了一次 DFS 遍历,每个节点恰好被访问一次,在每个节点上我们对其所有子节点进行常数时间的计算(求和与取最小值)。 - 空间复杂度:
O(n)。用于存储邻接表、dp数组以及递归调用栈(对于一棵退化成链的树,深度可能为n)。
总结
这个问题展示了如何利用树的特殊结构——无环和层次性,将看似复杂的全局优化问题(最小顶点覆盖)分解为一系列简单的局部子问题。通过巧妙的状态定义(dp[u][0/1])和后序遍历的计算顺序,我们能够自底向上地组合出全局最优解。这是树形动态规划的一个非常经典和优美的应用。