题解:洛谷 P14919 路径覆盖

【题目来源】

洛谷:P14919 [GESP202512 六级] 路径覆盖 - 洛谷

【题目描述】

给定一棵有 \(n\) 结点的有根树 \(T\),结点依次以 \(1,2,\ldots,n\) 编号,根结点编号为 \(1\)。方便起见,编号为 \(i\) 的结点称为结点 \(i\)

初始时 \(T\) 中的结点均为白色。你需要将 \(T\) 中的若干个结点染为黑色,使得所有叶子到根的路径上至少有一个黑色结点。将结点 \(i\) 染为黑色需要代价 \(c_i\),你需要在满足以上条件的情况下,最小化染色代价之和。

叶子是指 \(T\) 中没有子结点的结点。

【输入】

第一行,一个正整数 \(n\),表示结点数量。

第二行,\(n-1\) 个正整数 \(f_2,f_3,\ldots,f_n\),其中 \(f_i\) 表示结点 \(i\) 的父结点的编号,保证 \(f_i<i\)

第三行,\(n\) 个正整数 \(c_1,c_2,\ldots,c_n\),其中 \(c_i\) 表示将结点 \(i\) 染为黑色所需的代价。

【输出】

一行,一个整数,表示在满足所有叶子到根的路径上至少有一个黑色结点的前提下,染色代价之和的最小值。

【输入样例】

4
1 2 3
5 6 2 3

【输出样例】

2

【核心思想】

  1. 问题分析:给定一棵以 \(1\) 为根的有根树,每个节点染黑代价 \(c_i\)。要求所有叶子到根的路径上至少有一个黑色节点,最小化总代价。这是一个树形DP(后序遍历)问题,关键在于自底向上决策:每个节点要么自己染黑(代价 \(c_i\)),要么依赖子树中的黑色节点覆盖所有叶子路径。

  2. 算法选择

    • 树形DP(后序遍历)w[u] 表示以 \(u\) 为根的子树中,满足所有叶子到 \(u\) 的路径上有黑色节点的最小代价
    • 状态转移w[u] = min(c_u, Σw[v]),即要么在 \(u\) 染黑,要么在所有子树中分别满足条件
  3. 关键步骤

    • 读入数据\(n\),父节点数组 \(f[2..n]\),代价数组 \(c[1..n]\)
    • 建树:根据父节点关系建立有根树(子节点列表)
    • DFS 后序遍历(从根节点 \(1\) 开始):
      • \(u\) 是叶子节点,直接返回(w[u] = c_u 保持不变)
      • 遍历所有子节点 \(v\),递归 dfs(v)
      • sum += w[v](累加所有子树的最小代价)
      • w[u] = min(w[u], sum)(选择自己染黑或子树覆盖)
    • 输出 w[1]
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n)\),每个节点访问一次,每条边遍历一次
    • 空间复杂度:\(O(n)\),树结构和递归栈
  5. 树形DP最小覆盖的核心思想

    • 最优子结构:以 \(u\) 为根的子树的最小代价,只依赖于各子树的最小代价
    • 覆盖的两种策略
      • 自己染黑:代价 \(c_u\),一旦 \(u\) 染黑,其下所有叶子到根的路径都被覆盖(因为 \(u\) 在路径上)
      • 子树覆盖:不在 \(u\) 染黑,则每个子树必须独立满足条件(每个子树至少有一个黑色节点在其叶子到 \(u\) 的路径上),总代价为各子树代价之和
    • 后序遍历的必要性:子树的代价必须在父节点决策前计算完成,因此采用自底向上的DFS
    • 叶子节点的边界:叶子节点无子节点,sum = 0w[u] = min(c_u, 0) = 0?不对,代码中叶子直接返回,保持 w[u] = c_u。实际上叶子必须自己染黑(因为没有子节点可以依赖),但代码逻辑中 sum = 0min(c_u, 0) = 0 似乎有问题。仔细看代码:叶子节点 a[u].empty() 时直接返回,不执行 w[u] = min(w[u], sum),因此 w[u] 保持为 c_u。这是正确的,因为叶子必须自己染黑。
    • 适用于"树上最小点覆盖/路径覆盖"问题,核心在于后序遍历和最优子结构的利用

【解题思路】

img/Pasted image 20260511144039.png

【算法标签】

普及 #DFS-树

【代码详解】

#include <bits/stdc++.h>
using namespace std;

#define int long long
const int N = 100005;
int n;  // 节点总数
int w[N];  // 存储每个节点的权值
int x;  // 临时变量,用于输入父节点编号
vector<int> a[N];  // 邻接表,存储树的结构,a[u]存储节点u的所有子节点

// 深度优先搜索函数
// 参数u:当前处理的节点
void dfs(int u)
{
    // 如果当前节点是叶子节点(没有子节点),直接返回
    if (a[u].empty())
    {
        return;
    }

    int sum = 0;  // 存储所有子节点权值的总和

    // 遍历当前节点的所有子节点
    for (int i = 0; i < a[u].size(); i++)
    {
        // 递归处理子节点
        dfs(a[u][i]);

        // 累加子节点的权值
        sum += w[a[u][i]];
    }

    // 更新当前节点的权值:取自身权值和子节点权值总和的较小值
    w[u] = min(w[u], sum);
}

signed main()
{
    cin >> n;  // 输入节点总数

    // 输入树的边关系,构建树结构
    for (int i = 2; i <= n; i++)  // 从第2个节点开始,第1个节点是根节点
    {
        cin >> x;  // 输入节点i的父节点编号
        a[x].push_back(i);  // 将节点i添加到其父节点x的子节点列表中
    }

    // 输入每个节点的初始权值
    for (int i = 1; i <= n; i++)
    {
        cin >> w[i];
    }

    // 从根节点1开始进行深度优先搜索
    dfs(1);

    // 输出根节点的最终权值
    cout << w[1] << endl;

    return 0;  // 程序正常结束
}

【运行结果】

4
1 2 3
5 6 2 3
2
posted @ 2026-08-10 12:26  团爸讲算法  阅读(18)  评论(0)    收藏  举报