POJ 2054 Color a Tree#贪心(难,好题)

时间:2022-04-28 17:54:41

题目链接

代码借鉴此博:http://www.cnblogs.com/vongang/archive/2011/08/19/2146070.html

其中关于max{c[fa]/t[fa]}贪心原则,此博有很好的解释:http://www.cnblogs.com/rainydays/p/3271277.html

在此引用其中几段话:

试想,如果没有父节点排在节点之前的限制,那么这个题目非常简单,只需要将结点按照权值从大到小排列即可。加上了这个限制之后,如果权值最大的那个节点一旦满足了条件(父节点被排在了之前的某个位置),那么这个权值最大的节点一定要紧挨着这个父节点,即把这个权值最大的节点排在它所能排的最前面的位置。因为对于这个节点如果不受限制应该排在第一位,而有了限制,在满足了限制之后也应把它尽可能地排在前面。所以它一定是挨着父节点的。那么现在在最终的排列中我们确定了两个节点的前后相邻关系,将他们绑定在了一起。

试想如果保持这个相邻关系的同时去掉其他节点的限制,那么我们应该如何排列呢?我们假设绑定在一起的两节点是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。只需要不断这样做即可将问题缩减为只有一个节点。


以下为AC code:

//贪心:如果这棵树中有最大权值点X(非根),
//那么一旦X的父节点Y已经染色,就应该立刻染X
//于是X和Y合并成一个点集,
//新点集的权值=(新点集中所有点的权值和)/(新点集中点的个数)
//类似的,该点集可以看为一个点
//重复上述贪心思路,直到最后只剩下一个根r点集
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
using namespace std; const int N=1005; struct Node
{
int f;//父结点
int t;//时间
int c;//原权值
double w;//贪心权值(c/t)
}node[N]; int n,r; int findPos()
{
int pos;
double wmax=0;
for(int i=1;i<=n;i++)
if(node[i].w>wmax&&i!=r)
{
wmax=node[i].w;
pos=i;
}
return pos;
} int main()
{
while(scanf("%d%d",&n,&r)&&n+r)
{
int pos,fa,res=0;
for(int i=1;i<=n;i++)
{
scanf("%d",&node[i].c);
node[i].w=node[i].c;//将贪心权值w初始化为原权值c
node[i].t=1;//时间初始化为1
res+=node[i].c;//结果初始化为sum of所有原权值c
}
int u,v;
for(int i=1;i<n;i++)
{
scanf("%d%d",&u,&v);
node[v].f=u;//记录父结点
} for(int i=1;i<n;i++)
{
pos=findPos();//找到贪心权值最大的位置
node[pos].w=0;//置0,以便下次查找时跳过之
fa=node[pos].f;//fa为pos的父结点
res+=node[pos].c*node[fa].t;//res+=pos原权值*fa父结点时间(node[fa].t实际上代表,点集中有几个点)
for(int j=1;j<=n;j++)
if(node[j].f==pos)
node[j].f=fa;//若有j的父结点是pos,则将j的父结点改为fa,建立新集合
node[fa].t+=node[pos].t;//更新以fa为父结点的点集内点的个数(即加上以pos为父结点的点集内点的个数)
node[fa].c+=node[pos].c;//更新点集的话,那权值自然也要更新
node[fa].w=(double)node[fa].c/node[fa].t;//贪心原则:新点集权值和/新点集中点的个数
}
printf("%d\n",res);
}
return 0;
}