最小顶点覆盖问题的基于树形动态规划的精确算法(树上的精确解法)
字数 2665 2025-12-23 03:16:49

好的,我会从图论算法中随机挑选一个你从未听过的题目,为你细致讲解。今天我为你选择的题目是:

最小顶点覆盖问题的基于树形动态规划的精确算法(树上的精确解法)


题目描述

给定一棵(一个无环、连通的无向图),以及每个顶点上的一个正整数权重。我们的目标是找到这棵树的一个最小顶点覆盖(Minimum Vertex Cover, MVC),并使得这个顶点覆盖中所有顶点的权重之和最小

概念定义

  • 顶点覆盖:一个顶点集合,使得树中的每一条边都至少有一个端点(顶点)在这个集合里。
  • 最小顶点覆盖:在所有可能的顶点覆盖中,顶点权重之和最小的那一个。

输入

  • 一棵有 n 个顶点的树,通常以节点编号 1n 表示。
  • 每个顶点 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 值计算得来。

  1. 情况一:dp[u][0] (不选 u 进入覆盖)

    • 如果 u 不被选入覆盖,那么为了覆盖连接 u 与其子节点 v 的那条边 (u, v),我们必须选择它的子节点 v 进入覆盖!
    • 因此,对于 u 的每一个子节点 v,我们只能采用 v 被选中的状态,即 dp[v][1]
    • 状态转移方程为:
      dp[u][0] = Σ (dp[v][1]),对所有子节点 v 求和。
  2. 情况二: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] 依赖于其所有子节点 vdp 值,我们必须先计算子节点的值,再计算父节点。这自然对应着对树进行后序遍历(DFS)。

算法框架

  1. 将给定的树构建成一个邻接表 adj 来表示。
  2. 任选一个节点(如 1 号节点)作为根,进行 DFS。
  3. 在 DFS 过程中,当访问完节点 u 的所有子节点后,根据上述转移方程计算 dp[u][0]dp[u][1]
  4. 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]=1
    • dp[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 = 2
    • dp[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 = 7
    • dp[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])和后序遍历的计算顺序,我们能够自底向上地组合出全局最优解。这是树形动态规划的一个非常经典和优美的应用。

好的,我会从图论算法中随机挑选一个你从未听过的题目,为你细致讲解。今天我为你选择的题目是: 最小顶点覆盖问题的基于树形动态规划的精确算法(树上的精确解法) 题目描述 给定一棵 树 (一个无环、连通的无向图),以及每个顶点上的一个 正整数权重 。我们的目标是找到这棵树的一个 最小顶点覆盖 (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]) 。 伪代码 : 步骤 4:一个具体的例子 假设有一棵树如下(括号内为顶点权重): 我们以节点 1 为根进行计算。 计算过程(自底向上) : 叶子节点 4 和 5 : dp[4][0]=0 , dp[4][1]=1 dp[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 = 2 dp[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 = 7 dp[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] )和 后序遍历 的计算顺序,我们能够自底向上地组合出全局最优解。这是树形动态规划的一个非常经典和优美的应用。