原题链接
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 1≤i≤N,以结点 i i i 为根的子树都是好的,称整棵树是完美树。
你需要将整棵树变成完美树,为此你可以进行以下操作任意次(包括零次):选择任意一个结点 i i i ( 1 ≤ i ≤ N ) (1 ≤ i ≤ N) (1≤i≤N),改变结点 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) (1≤N≤105),表示树的结点个数。
接下来的
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(1≤Pi≤104,0≤Ki≤N),分别表示树上编号为
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]
C−f[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]));
}