首先是一个错误的贪心:每次选择一个当前可以染色的权值最大的节点进行染色。反例也容易找,只需要在一个权值很小的节点下面放一堆权值很大的节点,然后在另一颗子树中放一个比那个权值很小的点大一点点的节点就好了。
但是这个贪心可以给我们一些启示,当我们染了一个有儿子的点之后,这个点的权值最大的儿子一定会在接下来立刻染色。
既然这两个点是相继被染色的,那么可以将其合并成一个点,但是合并之后的节点的权值到底如何计算,这是个问题。
现在假设有三个节点,他们的权值分别是 a , b , c a, b, c a,b,c,已知前两个节点相继被染色,那么对于这三个节点染色的顺序,有两种决策,他们对答案的影响分别是:
- a + 2 b + 3 c a + 2b + 3c a+2b+3c
- c + 2 a + 3 b c + 2a + 3b c+2a+3b
运用简单的相减法对两个式子进行大小比较:
a
+
2
b
+
3
c
−
c
−
2
a
−
3
b
=
2
c
−
(
a
+
b
)
=
2
(
c
−
a
+
b
2
)
a + 2b + 3c - c - 2a - 3b = 2c - (a + b) = 2(c - \frac{a + b}{2})
a+2b+3c−c−2a−3b=2c−(a+b)=2(c−2a+b)
可以发现,实际上就是第三个节点的权值和前两个节点的权值平均值进行比较。
接着,我们可以搞出一种“等效权值”,用于记录合并之后的点团的性价比:
权
值
总
和
/
点
团
大
小
权值总和/点团大小
权值总和/点团大小
接着,我们每次找到一个“等效权值”最大的点团,将其和其父亲的点团进行合并,合并之前因为计算出这个点团因为在父亲之后被染色而对答案的影响。
#include <iostream>
#include <cstring>
#include <cstdio>
const int maxn = 1e5 + 5;
int n, r, fa[maxn], cnt[maxn], ans, sw[maxn];
double w[maxn];
int getMaxSw() {
int ret; double maxv = 0;
for (int i = 1; i <= n; i++)
maxv < w[i] && r != i ? maxv = w[ret = i] : maxv = maxv;
return ret;
}
int main() {
while (scanf("%d%d", &n, &r), n || r) {
ans = 0;
for (int i = 1; i <= n; i++) {
scanf("%d", &sw[i]);
w[i] = sw[i], cnt[i] = 1, ans += sw[i];
}
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v), fa[v] = u;
}
for (int i = 1; i < n; i++) {
int p = getMaxSw(), fat;
w[p] = 0, fat = fa[p];
ans += sw[p] * cnt[fat];
for (int j = 1; j <= n; j++)
if (fa[j] == p) fa[j] = fat;
sw[fat] += sw[p], cnt[fat] += cnt[p], w[fat] = (double)(sw[fat]) / cnt[fat];
}
printf("%d\n", ans);
}
return 0;
}