题目有难度。大致题意为:
给定一棵树,包括树中的节点数,每个节点的权值,以及每条边的情况。现在要给这棵树涂颜色。要求如下:
1)若给一棵树涂颜色,则其父节点必须已经涂完,树根必须首先涂颜色;
2)从时间1开始开始涂颜色,每涂完一个节点才能涂下一个节点,且每个节点的涂色时间为1个单位。
现在规定整个涂色过程的总权值为所有节点涂色时间点和其权值乘积之和。
题意很明确,但是贪心确实不好想。
分析如下:
首先我们知道由于条件1)则涂色顺序必定为拓扑序列。若没有1)的限制,那么题意就成为了有n个顶点,已知每个顶点的权值,现在要涂色,并使总权值最小。那么贪心策略在显然不过了,即:权值最大的放在前面。正是有了条件1)的限制,所以涂色顺序必须准守一定规则——即拓扑序列。这里我们可以这样猜想:如果父节点必须要在儿子节点之前涂色,那么权值最大的儿子节点就理应紧跟在父节点后别涂色,这样才能最大可能的和没有条件1)的最优情况靠近。
于是我们得出第一条猜想: 权值最大的节点必定紧跟在其父节点被涂色后涂色。
下面是证明: 这里令S表示权值最大的节点,其父节点记作P,假设涂P后先涂S,然后涂其它K个节点,即时间顺序为:PSn1……nK,花费时间为:
F1= T×Cp + (T+1)×Cs + {sigma[(T+1+i)×Cni],i=1->k}
而如果涂完S后,接着涂色的是K个节点,即时间顺序为:Pn1n2……nkS,花费时间为:
F2= T×Cp + {sigma[(T+i)×Cni],i=1->k} + (T+k+1)×Cs
F1-F2= {sigma(Cni),i=1->k} - k × Cs
因为S是树中权值最大的非根结点,
所以Cni<=Cs, {sigma(Cni),i=1->k}<=k×Cs,
所以F1-F2 < =0
k个结点n1,n2,...,nk的权值不会大于Cs,因为树中权值最大的非根结点为s, 而根节点的权值可能大于Cs,但由于p的存在所以p可能是根节点, n1,n2,...,nk不可能是根节点。
故得证,树中权值最大的非根结点为p,p的父结点为q,那么为q染色之后, 在下一个时间点为p染色,这样可以使得总费用最小。
这样以后我们就可以将权值最大的非根节点p与其父节点q合并为一个节点。合并后的新节点权值为(Cp+Cq)/2,父亲节点为q的父节点,儿子节点为q的儿子节点和p的儿子节点组合而成。
证明如下:
假设现在有两个选择:一是对q和p染色,然后对非q的后代的k个结点染色;
二是对非q的后代的k个结点(n1,n2,...,nk)染色,然后对q和p染色。
第二种选择相对于第一种选择费用之差为:
F2-F1=(Cq+Cp)×k-{sigma(Cni),i=1->k}×2
也就是说,第二种方案先对k个节点染色,相比第一种方案提前了2个时间点,
那么节省的费用是2×{sigma(Cni),i=1->k}; 后对q和p染色,
相比第一种方案延后了k个时间点,增加的费用是(Cq+Cp)×k。
哪一种选择会得到最优的结果?
(F2-F1)/(2×k) = (Cq+Cp)/2 - {sigma(Cni),i=1->k}/k
如果将q和p合并为一个结点x,Cx=(Cp+Cq)/2,n1,...,nk合并为一个节点y,
Cy={sigma(Cni),i=1->k}/k,那么我们当前面对的问题就是先染x还是先染y。
显然先染权值大的点然后染权值小的点,会得到最优解。
通过上式可知,使用结点权值的算术平均值作为合并得到的新结点的权值。
根据结论2的证明结论,每一次合并时,通过计算两个结点包含的原树中所有
结点的权值的算术平均值作为新的整体结点的权值。
下面是代码: 512K+157MS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define Max 1010
struct Node{ // 邻接表
int index; // 节点序号
struct Node *next; // 指向下一个儿子节点
}node[Max];
int num[Max]; //记录节点个数,包括合并后的节点个数
int pre[Max]; //记录节点父节点
int value[Max]; //记录节点权值
bool flag[Max]; //标记是否已经合并
int n,r; // 节点个数、根节点编号
int Sum; // 最少总权值
void add(int a,int b){ //构造邻接表,表中记录着节点a的儿子节点
struct Node *temp=(struct Node*)malloc(sizeof(struct Node));
temp->index=b;
temp->next=node[a].next;
node[a].next=temp;
}
int find(){ //查找value[i]/num[i]最小值标号,注意不能是根节点
double tt=-1.0;
int index;
for(int i=1;i<=n;i++)
if(!flag[i] && double(value[i])/num[i]>tt && i!=r){
tt=double(value[i])/num[i];
index=i;
}
return index;
}
void merge(int pra,int son){ // 合并父节点和子节点,成为新节点,同时更新子节点儿子节点的父亲节点情况
num[pra]+=num[son]; //合并后的节点数量
value[pra]+=value[son]; //合并后的节点权值
struct Node *point=node[son].next;
while(point!=NULL){ //更新子节点儿子节点父节点情况
pre[point->index]=pra;
point=point->next;
}
}
void cal(){ // 贪心算法求最少总权值
for(int i=1;i<n;i++){ //需要合并n-1次
int index=find(); // 查找最大值value[i]/num[i]
flag[index]=true; //标记已经合并
int pra=pre[index]; //求其父节点
while(flag[pra]) //直到没有合并为止
pra=pre[pra];
Sum+=(value[index]*num[pra]); //增加合并后的权值
merge(pra,index);//合并父节点和子节点
}
Sum+=value[r]; //最后由于所有除开根节点的节点的权值是实际上是乘以(实际路径-1),故还需要加上所有权值之和以及根节点的权值,而此时value正好为所有节点最初时的权值之和+根节点权值,故这里要加上
}
int main(){
while(scanf("%d%d",&n,&r),n|r){
for(int i=1;i<=n;i++){
scanf("%d",&value[i]); //输入节点权值
num[i]=1; //初始个数为1
node[i].next=NULL; // 初始化为NULL,头插法建立邻接表
}
int aa,bb;
memset(flag,0,sizeof(flag)); //初始化为未合并
for(int i=1;i<n;i++){ //输入边信息
scanf("%d%d",&aa,&bb);
add(aa,bb); //建立邻接表
pre[bb]=aa;
}
Sum=0; //初始化为0
cal(); //贪心计算最下总权值
printf("%d\n",Sum);//输出
}
return 0;
}