题解:洛谷 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\) 为根的有根树,每个节点染黑代价 \(c_i\)。要求所有叶子到根的路径上至少有一个黑色节点,最小化总代价。这是一个树形DP(后序遍历)问题,关键在于自底向上决策:每个节点要么自己染黑(代价 \(c_i\)),要么依赖子树中的黑色节点覆盖所有叶子路径。
-
算法选择:
- 树形DP(后序遍历):
w[u]表示以 \(u\) 为根的子树中,满足所有叶子到 \(u\) 的路径上有黑色节点的最小代价 - 状态转移:
w[u] = min(c_u, Σw[v]),即要么在 \(u\) 染黑,要么在所有子树中分别满足条件
- 树形DP(后序遍历):
-
关键步骤:
- 读入数据:\(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)(选择自己染黑或子树覆盖)
- 若 \(u\) 是叶子节点,直接返回(
- 输出
w[1]
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),每个节点访问一次,每条边遍历一次
- 空间复杂度:\(O(n)\),树结构和递归栈
-
树形DP最小覆盖的核心思想:
- 最优子结构:以 \(u\) 为根的子树的最小代价,只依赖于各子树的最小代价
- 覆盖的两种策略:
- 自己染黑:代价 \(c_u\),一旦 \(u\) 染黑,其下所有叶子到根的路径都被覆盖(因为 \(u\) 在路径上)
- 子树覆盖:不在 \(u\) 染黑,则每个子树必须独立满足条件(每个子树至少有一个黑色节点在其叶子到 \(u\) 的路径上),总代价为各子树代价之和
- 后序遍历的必要性:子树的代价必须在父节点决策前计算完成,因此采用自底向上的DFS
- 叶子节点的边界:叶子节点无子节点,
sum = 0,w[u] = min(c_u, 0) = 0?不对,代码中叶子直接返回,保持w[u] = c_u。实际上叶子必须自己染黑(因为没有子节点可以依赖),但代码逻辑中sum = 0时min(c_u, 0) = 0似乎有问题。仔细看代码:叶子节点a[u].empty()时直接返回,不执行w[u] = min(w[u], sum),因此w[u]保持为c_u。这是正确的,因为叶子必须自己染黑。 - 适用于"树上最小点覆盖/路径覆盖"问题,核心在于后序遍历和最优子结构的利用
【解题思路】

【算法标签】
普及 #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
浙公网安备 33010602011771号