记录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)问题。

  1. 问题转化
    题目要求所有叶子结点到根结点的路径上至少有一个黑色结点。我们可以将这个问题分解到每一棵子树中去解决。对于任意一个结点 u,要满足它子树内所有叶子到根的路径合法,只有两种最优的决策方案:

    • 方案一:结点 u 自己染成黑色。如果 u 是黑色的,那么从 u 的子树中任意叶子向上走到 u 时,路径上就已经有了黑色结点。此时,u 的子结点们无需再为了“向上覆盖”而承担额外代价,它们只需要各自管好自己的子树即可。
    • 方案二:结点 u 保持白色。如果 u 不染黑,那么为了覆盖 u 的子树中所有叶子的路径,u 的所有子结点 v 必须各自独立地满足条件(即 v 的子树内的叶子到 v 的路径必须有黑点)。
  2. 状态定义与转移
    定义 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] 先递归子结点再合并 确保在计算父结点最优解时,所有子结点的最优解已经计算完毕
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐