题目大意:
有一棵带着 N N 个节点的树,每个节点都有一个代价基值。现在要给每个点染色,第一个染根节点,其余的节点染色的时候其父节点必须已染色。每个节点染色会用掉一个时间单位,每个节点染色的代价是染完此节点时的当前时间 T∗Ci T ∗ C i 。问染完全部节点所需要的最小代价。
1≤N≤1000
1
≤
N
≤
1000
1<=Ci<=500
1
<=
C
i
<=
500
分析:
一道非常有趣的贪心题,我想了一个上午,然后不会,然后借鉴了一些大佬的博客,才明白正确的贪心原则,
下面借鉴大佬的分析:
试想,如果没有父节点排在节点之前的限制,那么这个题目非常简单,只需要将结点按照权值从大到小排列即可。加上了这个限制之后,如果权值最大的那个节点一旦满足了条件(父节点被排在了之前的某个位置),那么这个权值最大的节点一定要紧挨着这个父节点,即把这个权值最大的节点排在它所能排的最前面的位置。因为对于这个节点如果不受限制应该排在第一位,而有了限制,在满足了限制之后也应把它尽可能地排在前面。所以它一定是挨着父节点的。那么现在在最终的排列中我们确定了两个节点的前后相邻关系,将他们绑定在了一起。
试想如果保持这个相邻关系的同时去掉其他节点的限制,那么我们应该如何排列呢?我们假设绑定在一起的两节点是a和b。现有一个另外的节点x,我们看两种排列xab,abx对最终的计算结果有什么影响。x*i+a*(i+1)+b*(i+2); a*i + b*(i+1) + x*(i+2)。后者减去前者等于2x-(a+b)。即将x从ab之前挪到ab之后,ab各左移1位,结果减小a+b。x右移2位结果增加2x。因此两者谁在前谁在后我们只需要比较a+b和2x即可,也可以比较(a+b)/2和x。
将这个定理进行一下推广,绑定在一起的不一定是两个节点,可以是一个更长的序列,与这个序列进行比较看谁放在前面的也可以是一个序列。设一个序列有n1个节点,第二个序列有n2个节点。那么我们比较两者谁放在前面的时候需要比较的是(n1个权值之和×n2)和(n2个权值之和×n1)。即左移和右移产生的结果变化。当然也可以比较(n1个权值之和/n1)和(n2个权值之和/n2)。
我们可以再次进行推广,如果我们要排列的不是节点,而是许多序列的话,那么我们只需要计算每个序列权值的平均数(例如:n个节点的序列,要计算n个权值之和/n),然后按照这个平均数从大到小排列即可使得计算结果最小。这样就可以让序列与节点有了一个统一的衡量值——平均数。
这样一来,我们就可以将上面的绑定两节点的操作看成是将问题规模缩小的操作,在帮定两节点的同时我们在树中也将两节点合并,变为一个节点,即将子节点的孩子变为父节点的孩子。然后合并后的节点的权值是合并在这个节点中的所有节点的权值的平均数。我们成功的将问题规模减小了1。只需要不断这样做即可将问题缩减为只有一个节点。
代码:
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define N 1005
using namespace std;
struct Node { int fa, t, c; double w; }a[N];
int n, r;
int find()
{
int id;
double max = 0;
for (int i = 1; i <= n; i++)
if (a[i].w > max && i != r) max = a[i].w, id = i;
return id;
}
int main()
{
while (~scanf("%d%d", &n, &r))
{
if (n == 0 && r == 0) break;
int ans = 0;
for (int i = 1; i <= n; i++)
{
scanf("%d", &a[i].c);
a[i].w = a[i].c;
a[i].t = 1;
ans += a[i].c;
}
int u, v;
for (int i = 1; i < n; i++)
{
scanf("%d%d", &u, &v);
a[v].fa = u;
}
for (int i = 1; i < n; i++)
{
int y = find();
a[y].w = 0;
int x = a[y].fa;
ans += a[y].c * a[x].t;
for (int j = 1; j <= n; j++)
if (a[j].fa == y) a[j].fa = x;
a[x].c += a[y].c,
a[x].t += a[y].t,
a[x].w = (double)a[x].c / a[x].t;
}
printf("%d\n", ans);
}
return 0;
}