2023 GPLT 天梯赛 L3-035 完美树 —— 树形DP,状态机,贪心

原题链接

 https://pintia.cn/problem-sets/994805046380707840/exam/problems/1649748772845703169

题目大意

给定一棵有 N N N 个结点的树(树中结点从 1 1 1 N N N 编号,根结点编号为 1 1 1)。每个结点有一种颜色,或为黑,或为白。

若子树中黑色结点与白色结点的数量之差的绝对值不超过 1 1 1,称以结点 u u u 为根的子树是好的。若对于所有 1 ≤ i ≤ N 1 ≤ i ≤ N 1iN,以结点 i i i 为根的子树都是好的,称整棵树是完美树

你需要将整棵树变成完美树,为此你可以进行以下操作任意次(包括零次):选择任意一个结点 i i i ( 1 ≤ i ≤ N ) (1 ≤ i ≤ N) (1iN),改变结点 i i i 的颜色(若结点 i i i 目前是黑色则将其改为白色,若结点 i i i 目前是白色则将其改为黑色)。这次操作的代价为 P i P_i Pi,求将给定的树变为完美树的最小代价。

注:以结点 i i i 为根的子树,由结点 i i i 以及结点 i i i 的所有后代结点组成。

输入格式

输入第一行为一个数 N N N ( 1 ≤ N ≤ 1 0 5 ) (1≤N≤10^5) (1N105),表示树的结点个数。

接下来的 N N N 行,第 i i i 行的前三个数为 C i ​ , P i ​ , K i ​ ( 1 ≤ P i ​ ≤ 1 0 4 , 0 ≤ K i ​ ≤ N ) C_i​,P_i​,K_i​ (1≤P_i​≤10^4,0≤K_i​≤N) Ci,Pi,Ki(1Pi104,0KiN),分别表示树上编号为 i i i 的结点的初始颜色( 0 0 0 为白色, 1 1 1 为黑色)、变换颜色的代价及孩子结点的数量。紧跟着有 K i K_i Ki​ 个数,为孩子结点的编号。数字均用一个空格隔开,所有的编号保证在 1 1 1 N N N 里,且不会有环。
数据中只包含一棵树。

输出格式

输出一行一个数,表示将树 T T T 变为完美树的最小代价。

输入样例

10
1 100 3 2 3 4
0 20 1 7
0 5 2 5 6
0 8 1 10
0 7 0
0 2 0
1 1 2 8 9
0 15 0
0 13 0
1 8 0

输出样例

15

题解

这是一道非常典型的树形DP问题,不熟悉此知识点的读者可以回顾一下这两道题:打家劫舍III和没有上司的舞会。

如果一棵树(或子树)是“好的”,最多可能有三种情况:树上的黑色结点比白色结点多 1 1 1 个,树上的黑色结点和白色结点数量相等,树上的黑色结点比白色结点少 1 1 1 个。而这又与树上结点的个数有关:如果结点数量为偶数,它还是一棵好的树,只能要求白色结点和黑色结点的数量相同;否则如果结点数量是奇数,白色和黑色结点的数量必须相差一个。

由于最多只有 3 3 3 种情况,我们就定义 f [ n ] [ 3 ] f[n][3] f[n][3] 代表将节点 n n n 为根的子树变成完美树需要花费的最少代价,不妨认为 f [ n ] [ 0 ] f[n][0] f[n][0] 代表树中黑色比白色结点多 1 1 1 个(后面简称为染黑), f [ n ] [ 1 ] f[n][1] f[n][1] 代表二者数量相等, f [ n ] [ 2 ] f[n][2] f[n][2] 代表白色比黑色结点多一个(后面简称为染白)。

下面则是树形DP:对于整棵树来说,设它的根为 r o o t root root ,进行后序遍历来优先处理所有子树并计算子树中结点的个数。如果以 t t t 为根的子树,它的结点个数为偶数,毫无疑问它必须处理成黑色和白色结点数量相等,处理的代价为 f [ t ] [ 1 ] f[t][1] f[t][1] ; 如果结点的个数为奇数,它就有 2 2 2 种处理方式,分别是 f [ t ] [ 0 ] f[t][0] f[t][0] f [ t ] [ 2 ] f[t][2] f[t][2]。接下来就是状态转移的过程:

Case 1:如果所有子树加在一起,总共白色结点比黑色结点多 2 2 2 个,此时根必须要是黑的,而且只能转移到 f [ r o o t ] [ 2 ] f[root][2] f[root][2];
Case 2: 如果所有子树加在一起,总共白色结点比黑色结点多 1 1 1 个,此时根也必须要是黑的,而且只能转移到 f [ r o o t ] [ 1 ] f[root][1] f[root][1];
Case 3: 如果所有子树加在一起,白色结点和黑色结点的数量相等,此时根的颜色任意,可以转移到 f [ r o o t ] [ 0 ] f[root][0] f[root][0] f [ r o o t ] [ 2 ] f[root][2] f[root][2];
Case 4: 如果所有子树加在一起,总共黑色结点比白色结点多 1 1 1 个,此时根必须要是白的,而且只能转移到 f [ r o o t ] [ 1 ] f[root][1] f[root][1];
Case 5: 如果所有子树加在一起,总共黑色结点比白色结点多 2 2 2 个,此时根也必须要是白的,而且只能转移到 f [ r o o t ] [ 0 ] f[root][0] f[root][0]

只有这五种情况,其他情况不能成立,状态转移是十分清晰的。如果所需根的颜色和目前根的颜色不同,则需要额外支付给根染色的代价。

最后来看究竟哪些子树染白,哪些子树染黑。这里是一个贪心:
假设结点个数为奇数个的子树有 k k k 棵,我们不妨首先假设全部子树都染白,此时白色结点多了 k k k 个,设总代价是 C C C。之后,我每把 1 1 1 棵子树 t t t 变成染黑的情况,总代价就会减去染白的代价加上染黑的代价,即 C − f [ t ] [ 0 ] + f [ t ] [ 2 ] C-f[t][0]+f[t][2] Cf[t][0]+f[t][2]。这个决策和染黑的顺序无关,只和选择哪些树染色有关,因此贪心算法是明显成立的,只需要选择 f [ t ] [ 2 ] − f [ t ] [ 0 ] f[t][2]-f[t][0] f[t][2]f[t][0] 最小的若干子树染就可以。每染 1 1 1 棵子树,多余的白色结点数量就会减少 2 2 2,因此可以选择用优先队列(小根堆)维护这个过程,持续选择堆顶元素代表的树染色就可以尽可能降低代价,当白色结点与黑色结点的数量差达到 2 , 1 , 0 , − 1 , − 2 2,1,0,-1,-2 2,1,0,1,2 时,根据上面的 5 5 5 个Case即可开始状态转移。这样就可以通过本题。

相关链接

树形DP是有比较经典的套路的:定义状态、后序遍历、状态机/状态转移,不熟悉的同学可以做一下这几道题练习一下。

没有上司的舞会 https://www.luogu.com.cn/problem/P1352
打家劫舍III https://leetcode.cn/problems/house-robber-iii/
最小化旅行的价格总和 https://leetcode.cn/problems/minimize-the-total-price-of-the-trips/

AC代码

#include <bits/stdc++.h>
using namespace std;
vector<int> v[100050];
int color[100050];
int number[100050];
int cost[100050];
int f[100050][3]; // 0-黑比白多1 1-两者相等 2-白比黑多1
void traversal(int root)
{
    for(auto k:v[root]) traversal(k);
    priority_queue<int> pq;
    int sum = 0, cnt = 0; 
    for(auto k:v[root]){
        number[root] += number[k];
        //先默认所有奇数个结点的子树都取白的比黑的多1个
        if(number[k]%2==1){ //只能由1或者3转移过来
            pq.push(f[k][2]-f[k][0]); //把替换代价最小的拿过来
            sum += f[k][2];
            cnt++;
        } else sum += f[k][1]; //否则只能由2转移过来
    }
    number[root]++;
    //此时分别计算f[root][i]
    while(cnt>=-2){
        if(cnt==2) { //白的比黑的多2个,根必须是黑的
            if(color[root] == 1) f[root][2] = min(f[root][2],sum);
            else f[root][2] = min(f[root][2], sum + cost[root]);
        }
        if(cnt==1) { //白的比黑的多1个,根必须是黑的
            if(color[root] == 1) f[root][1] = min(f[root][1],sum);
            else f[root][1] = min(f[root][1], sum + cost[root]);
        }
        if(cnt==0) { //白的和黑的相等,这是任意的
            if(color[root] == 1){
                f[root][0] = min(f[root][0], sum);
                f[root][2] = min(f[root][2], sum + cost[root]);
            } else {
                f[root][0] = min(f[root][0], sum + cost[root]);
                f[root][2] = min(f[root][2], sum);               
            }
        }
        if(cnt==-1) { //白的比黑的少1个,根必须是白的
            if(color[root] == 1) f[root][1] = min(f[root][1],sum + cost[root]);
            else f[root][1] = min(f[root][1], sum);
        }
        if(cnt==-2) { //白的比黑的少2个,根必须是白的
            if(color[root] == 1) f[root][0] = min(f[root][0],sum + cost[root]);
            else f[root][0] = min(f[root][0], sum);                
        }
        if(!pq.size()) break;
        sum -= pq.top();
        pq.pop();
        cnt -= 2;
    }
}
map<int,int> m;
int main()
{
    int n;
    cin >> n;
    memset(f,0x3f,sizeof f);
    for(int i=1;i<=n;i++){
        int k;
        scanf("%d %d %d",&color[i],&cost[i],&k);
        for(int j=0;j<k;j++) {
            int x;
            scanf("%d",&x);
            v[i].push_back(x);
            m[x] = i;
        }
    }
    int root;
    for(int i=1;i<=n;i++) {
        if(m[i] == 0){
            root = i;
            break;
        }
    }
    traversal(root);
    cout << min(f[root][1],min(f[root][0],f[root][2]));
}
  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值