P14919 [GESP202512 六级] 路径覆盖
·
记录134
#include<bits/stdc++.h> // 引入C++万能头文件,包含vector等常用标准库
using namespace std; // 使用标准命名空间std
#define ll long long // 定义长整型别名ll,因为代价c[i]最大为1e9,累加容易溢出int
const int N = 1e5 + 10; // 定义常量N,表示结点数量的最大范围
int n; // 定义全局变量n,表示结点的总数
vector<int> g[N]; // 定义邻接表g,g[u]存储结点u的所有子结点
ll c[N]; // 定义数组c,c[i]表示将结点i染为黑色所需的代价
ll dp[N]; // 定义dp数组,dp[i]表示以i为根的子树满足条件的最小代价
// 定义深度优先搜索函数,u表示当前正在处理的结点
void dfs(int u){
ll sum=0; // 定义变量sum,用来累加当前结点所有子结点的dp值
bool is_leaf=true; // 定义布尔变量is_leaf,标记当前结点是否为叶子结点
for(int v:g[u]){ // 遍历当前结点u的所有子结点v
is_leaf=false; // 只要有子结点,就说明u不是叶子结点
dfs(v); // 递归处理子结点v,自底向上计算子结点的dp值
sum+=dp[v]; // 将子结点v的最小代价累加到sum中
}
if(is_leaf){ // 如果当前结点u是叶子结点
dp[u]=c[u]; // 叶子结点必须自己染色,否则它到自己的路径就没有黑点
} else { // 如果当前结点u不是叶子结点
// 状态转移:取“自己染黑”和“子结点们自己解决”两者中的较小值
dp[u]=min(c[u], sum);
}
}
int main(){ // 主函数入口
ios::sync_with_stdio(false); // 关闭标准流同步,提升输入输出效率
cin.tie(0); // 解除cin与cout的绑定,加快读取速度
cin>>n; // 读入结点的数量n
// 循环读入n-1个父结点信息,并建立邻接表
for(int i=2;i<=n;i++){
int f; // 定义临时变量f,表示结点i的父结点
cin>>f; // 读入结点i的父结点编号
g[f].push_back(i); // 在邻接表中将i加入f的子结点列表
}
// 循环读入n个结点的染色代价
for(int i=1;i<=n;i++){
cin>>c[i]; // 读入结点i的染色代价
}
dfs(1); // 从根结点1开始进行DFS,计算整棵树的最小代价
cout<<dp[1]<<"\n"; // 输出以根结点1为根的子树的最小代价,即最终答案
return 0; // 程序正常结束
}
题目传送门
https://www.luogu.com.cn/problem/P14919
前言
我是一名专注信奥赛(CSP-J/S、NOIP)的教练。
- 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
- 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
核心解题思路
这道题是一道经典的树形动态规划(Tree DP)问题。
-
问题转化:
题目要求所有叶子结点到根结点的路径上至少有一个黑色结点。我们可以将这个问题分解到每一棵子树中去解决。对于任意一个结点u,要满足它子树内所有叶子到根的路径合法,只有两种最优的决策方案:- 方案一:结点
u自己染成黑色。如果u是黑色的,那么从u的子树中任意叶子向上走到u时,路径上就已经有了黑色结点。此时,u的子结点们无需再为了“向上覆盖”而承担额外代价,它们只需要各自管好自己的子树即可。 - 方案二:结点
u保持白色。如果u不染黑,那么为了覆盖u的子树中所有叶子的路径,u的所有子结点v必须各自独立地满足条件(即v的子树内的叶子到v的路径必须有黑点)。
- 方案一:结点
-
状态定义与转移:
定义dp[u]为:使得以u为根的子树中,所有叶子到u的路径上至少有一个黑色结点的最小代价。- 如果
u是叶子结点:它必须自己染黑,dp[u] = c[u]。 - 如果
u不是叶子结点:取上述两种方案的最小值,即dp[u] = min(c[u], sum(dp[v])),其中sum(dp[v])是u所有子结点v的dp值之和。
- 如果
代码分块详细解释
1. 头文件、常量与全局变量定义
#include<bits/stdc++.h>
using namespace std;
#define ll long long // 定义长整型别名 ll,因为代价 c[i] 最大为 1e9,累加容易溢出 int
const int N = 1e5 + 10; // 定义常量 N,表示结点数量的最大范围
int n; // 定义全局变量 n,表示结点的总数
vector<int> g[N]; // 定义邻接表 g,g[u] 存储结点 u 的所有子结点
ll c[N]; // 定义数组 c,c[i] 表示将结点 i 染为黑色所需的代价
ll dp[N]; // 定义 dp 数组,dp[i] 表示以 i 为根的子树满足条件的最小代价
- 详细分析:由于染色代价
c_i最大可达 10^9,在极端情况下累加可能会超出int的范围,因此c数组和dp数组必须使用long long类型。vector<int> g[N]用于存储树的子结点关系,因为题目保证了父结点编号小于子结点(f_i < i),这实际上已经给出了一种拓扑序,但使用 DFS 递归处理更加直观。
2. 深度优先搜索(DFS)与状态转移
// 定义深度优先搜索函数,u 表示当前正在处理的结点
void dfs(int u){
ll sum = 0; // 定义变量 sum,用来累加当前结点所有子结点的 dp 值
bool is_leaf = true; // 定义布尔变量 is_leaf,标记当前结点是否为叶子结点
for(int v : g[u]){ // 遍历当前结点 u 的所有子结点 v
is_leaf = false; // 只要有子结点,就说明 u 不是叶子结点
dfs(v); // 递归处理子结点 v,自底向上计算子结点的 dp 值
sum += dp[v]; // 将子结点 v 的最小代价累加到 sum 中
}
if(is_leaf){ // 如果当前结点 u 是叶子结点
dp[u] = c[u]; // 叶子结点必须自己染色,否则它到自己的路径就没有黑点
} else { // 如果当前结点 u 不是叶子结点
// 状态转移:取“自己染黑”和“子结点们自己解决”两者中的较小值
dp[u] = min(c[u], sum);
}
}
- 详细分析:这是树形 DP 的核心。
- 自底向上计算:通过
dfs(v)先递归处理完所有子结点,拿到子结点的最优解dp[v]。 - 叶子结点处理:如果
u是叶子,它没有任何子结点可以分担“覆盖路径”的责任,所以dp[u]只能等于它自身的染色代价c[u]。 - 非叶子结点贪心:如果
u不是叶子,我们面临抉择。要么花费c[u]染黑自己,一劳永逸地覆盖所有子树路径;要么自己不染,把压力下放,让所有子结点各自承担自己的子树代价(即sum)。取两者较小值即为dp[u]的最优解。
- 自底向上计算:通过
3. 主函数:建图与初始化
int main(){
ios::sync_with_stdio(false); // 关闭标准流同步,提升输入输出效率
cin.tie(0); // 解除 cin 与 cout 的绑定,加快读取速度
cin >> n; // 读入结点的数量 n
// 循环读入 n-1 个父结点信息,并建立邻接表
for(int i = 2; i <= n; i++){
int f; // 定义临时变量 f,表示结点 i 的父结点
cin >> f; // 读入结点 i 的父结点编号
g[f].push_back(i); // 在邻接表中将 i 加入 f 的子结点列表
}
// 循环读入 n 个结点的染色代价
for(int i = 1; i <= n; i++){
cin >> c[i]; // 读入结点 i 的染色代价
}
- 详细分析:题目给出的输入是
f_2, f_3, ..., f_n,即每个结点的父结点。我们在读入时直接将其转化为子结点列表g[f].push_back(i),从而构建出一棵以 1 为根的有向树。
4. 启动 DP 与输出结果
dfs(1); // 从根结点 1 开始进行 DFS,计算整棵树的最小代价
cout << dp[1] << "\n"; // 输出以根结点 1 为根的子树的最小代价,即最终答案
return 0; // 程序正常结束
}
- 详细分析:调用
dfs(1)从整棵树的根结点开始递归。当递归返回时,dp[1]中存储的就是使得整棵树(以 1 为根)所有叶子到根的路径上都有黑色结点的最小总代价。
核心逻辑总结表
| 代码模块 | 核心变量/操作 | 精炼作用 | 解决的痛点 |
|---|---|---|---|
| 数据类型 | #define ll long long |
使用 64 位长整型 | 防止 105 个结点、每个代价 10^9 累加时发生数据溢出 |
| 树的存储 | g[f].push_back(i) |
建立父到子的有向边 | 将输入的父结点数组转化为便于 DFS 遍历的邻接表 |
| 叶子判断 | bool is_leaf |
标记当前结点是否有子结点 | 区分基础状态(叶子必须染)和递推状态(非叶子做抉择) |
| 状态转移 | dp[u] = min(c[u], sum) |
树形 DP 核心贪心公式 | 在“自己承担代价”与“下放代价给子结点”之间做出最优选择 |
| 自底向上 | dfs(v); sum += dp[v] |
先递归子结点再合并 | 确保在计算父结点最优解时,所有子结点的最优解已经计算完毕 |
更多推荐

所有评论(0)