题解:GESP202512五级T1

题解:GESP202512五级T1

前言

题目传送门

思路讲解

如果直接遍历每一个节点暴力,肯定是不行的,我们考虑动态规划的思路

在树上做 \(DP\) 就是树形 \(DP\),不过这个转化过程比较简单,我们可以用拓扑排序和 \(DP\) 来做

定义状态

我们定义 \(dp_i\) 以编号为 \(i\) 的节点为根节点的子树中全部叶子节点到根节点的路径上都至少有一个黑色节点所需要的最小代价(有亿点点抽象)。

那么我们的答案就是 \(dp_1\)。(因为它是根节点)

状态转移方程

假设一棵树长这样:

那么我们会发现,如果想要节点 567 到到根节点的路径上都至少有一个黑色节点,可以将他们全部染色成黑色节点,代价就是 \(c_6+c_6+c_7\)

但是如果把节点 2 染色了,也可以达到目的,代价为 \(c_2\)

注意到达到这个目的的最小代价是他们的最小值,也就是 \(min(c_2,c_5+c_6+c_7)\),注意到这个东西所代表的不就是 \(dp_2\) 吗?

得出结论,\(dp_i\) 等于 \(c_i\) 与其所有子节点的 \(dp\) 之和的最小值。

所以状态转移代码如下:

int cur = 0; //其子节点的DP之和 
for (int i = 0; i < g[now].size(); i++)
{
    cur += dp[g[now][i]];
}
if (cur == 0) dp[now] = c[now]; //如果他没有子节点,那只能是c[now]  
else dp[now] = min(c[now], cur); //状态转移 

代码流程

先输入,顺便用一个 \(sz\) 数组来存一个节点的子节点个数, \(g\) 数组来存一个节点的子节点的编号。

输入完把叶子结点入队,同时这个叶子节点的 \(dp\) 值等于 \(c_i\)

接着开始拓扑排序,先提取队首,删除队首。

\(now\) 的父节点子节点数量减一(注意是sz[fa[now]]--\(g\) 数组不用删,因为一会算父节点的 \(dp\) 值时还需要用到)。

如果父节点没子节点了,就把父节点入队。

接着就是状态转移。

最后输出 \(dp_1\) 即可。

AC Code

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 10;
int n;
int fa[MAXN]; // 记录父节点编号
int sz[MAXN]; // 记录子节点个数
int c[MAXN];  // 染色代价
int dp[MAXN];
vector<int> g[MAXN]; // 记录儿子节点编号
queue<int> q;
int main()
{
    cin >> n;
    // 输入
    for (int i = 2; i <= n; i++)
    {
        cin >> fa[i];
        g[fa[i]].push_back(i); // 记录儿子节点编号
        sz[fa[i]]++;           // 记录子节点个数
    }
    for (int i = 1; i <= n; i++)
    {
        cin >> c[i];
        // 输入
    }
    for (int i = 1; i <= n; i++)
    {
        if (sz[i] == 0)
        {
            q.push(i);
            dp[i] = c[i]; // 把叶子节点入队
        }
    }
    while (!q.empty()) // 拓扑排序
    {
        int now = q.front();  // 队首元素
        q.pop();              // 队首出队
        sz[fa[now]]--;        // 父节点的子节点个数减1
        if (sz[fa[now]] == 0) // 如果父节点的子节点个数为0
        {
            q.push(fa[now]); // 父节点入队
        }
        int cur = 0; // 其子节点的DP之和
        for (int i = 0; i < g[now].size(); i++)
        {
            cur += dp[g[now][i]];
        }
        if (cur == 0)
            dp[now] = c[now]; // 如果他没有子节点,那只能是c[now]
        else
            dp[now] = min(c[now], cur); // 状态转移
    }
    cout << dp[1] << endl; // 输出结果
    return 0;
}

时空复杂度分析

时间复杂度

输入: \(O(n)\)

初始化: \(O(n)\)

拓扑排序一共遍历 \(n\) 个节点,内部循环也一共只会遍历 \(n\) 个节点,除去系数,时间复杂度为 \(O(n)\)

总时间复杂度: \(O(n)\)

空间复杂度

\(fa\) 数组: \(O(n)\)

\(sz\) 数组: \(O(n)\)

\(c\) 数组: \(O(n)\)

\(dp\) 数组: \(O(n)\)

\(g\) 数组:邻接表,复杂度为 \(O(n)\)

总空间复杂度: \(O(n)\)

posted @ 2025-12-27 17:11  fengjunxiao2014  阅读(35)  评论(0)    收藏  举报