AcWing 115 给树染色

题目描述:

一颗树有 n 个节点,这些节点被标号为:1,2,3…n,每个节点 i 都有一个权值 A[i]。现在要把这棵树的节点全部染色,染色的规则是:根节点R可以随时被染色;对于其他节点,在被染色之前它的父亲节点必须已经染上了色。每次染色的代价为T*A[i],其中T代表当前是第几次染色。求把这棵树染色的最小总代价。

输入格式

第一行包含两个整数 n 和 R ,分别代表树的节点数以及根节点的序号。第二行包含 n 个整数,代表所有节点的权值,第 i 个数即为第 i 个节点的权值 A[i]。接下来n-1行,每行包含两个整数 a 和 b ,代表两个节点的序号,两节点满足关系: a 节点是 b 节点的父节点。除根节点外的其他 n-1 个节点的父节点和它们本身会在这 n-1 行中表示出来。同一行内的数用空格隔开。

输出格式

输出一个整数,代表把这棵树染色的最小总代价。

数据范围

1≤n≤1000,1≤A[i]≤1000

输入样例:

5 1
1 2 1 2 4
1 2
1 3
2 4
3 5

输出样例:

33

分析:

染色的代价随着时间的推移越来越高,所以需要将权值大的结点尽可能先染色。如果题目没要求父结点必须在子结点之前染色的话,那么我们完全可以将树中的结点按照权值大小排序,先给权值大的结点染色。我们在选择染色结点时,是否可以在当前节点所有孩子中选择权值最大那个结点去染色呢?想象一下一棵树,根结点左孩子很小,但是左孩子下面的孩子很大;根结点右孩子比左孩子大一点,但是其孩子都很小。这种染色方案显然是错误的。

上述染色方案给我们的启示是,尽管我们无法给权值最大的结点最先染色,但是一旦他的父结点被染色,那么他就必然是下一个被染色的结点,这样我们就找到了染色顺序中相邻的两个结点。考察 a,b, c三个节点,其中b的染色时间紧挨着a。如果先染a和b,再染c,代价是a+2b+3c;反过来先染c的代价是c+2a+b。左边=a+2b+3c,右边=c+2a+3b,左边-右边=2c-a-b。当c>(a+b)/2时,即c的权值大于a和b的平均权值时,先染c代价更小,否则就先染a和b。

通过上面分析,本题可以这样解决:先找到权值最大的结点,合并进其父结点,并求其均值,然后再在剩下结点中找出均值最大的结点,继续合并,直至合并进根结点,该过程类似于哈夫曼树的构建过程。

下面详细分析下程序的设计过程。

首先确定结点的结构体要存储什么,结点的权值和父结点需要存储,结点的平均值也需要存储,计算已合并结点的均值的时候,同样需要合并的个数s,所以也需要存储。

需要一个查找最大均值的函数,该函数设计时要注意不能算上根结点,因为无法将根结点合并进其父结点(没有父结点)。

另外,有特别多的细节需要考虑。总代价ans的初值最好设置为所有结点权值和,一方面是因为根结点没有合并进其父结点,不好计算,另一方面是因为所有结点至少要对总代价贡献出权值一次。合并某结点到其父结点时,父结点的s(结点中包含的所有合并结点个数)应该加上待合并结点的s而不是简单的加上1。为什么结构体中不单独存储合并结点的总权值呢,因为某结点的孩子结点合并进它本身后,它原来的权值便没有存在的必要了,完全可以当成累加的权值使用,父结点的总权值v合并时加上的也是待合并结点的v。合并还要注意的是,一个结点一旦合并进其它结点,它的均值要置为-1,防止重复被当做最大值,并且它的孩子结点的父结点都应该更新为新的合并结点。最容易出错的是每合并一个结点,总代价ans需要加上多少?显然不只是加上待合并结点的权重而已。设root为树根,其左孩子已经完全合并进root了,此时root中包含的结点个数为s,其右孩子在合并进root中时,已经是第s+1次被染色了,ans本该是加上(s+1)*v,但是ans初始化时已经加上了一次各个结点的值,所以只用加上s*v即可,也就是某结点合并进其父结点时,ans要加上其权值v乘以其父结点的s。最后需要注意的是求结点均值时需要加上double强转,防止精度损失。所有需要注意的点均已阐明,具体实现见代码。

#include <iostream>
using namespace std;
int n,r,a,b;
struct Node{
    int v,s,f;
    double avg;
}t[1005];
int find(){
    int k = -1;
    double avg = 0;
    for(int i = 1;i <= n;i++){
        if(i != r && t[i].avg > avg){
            avg = t[i].avg;
            k = i;
        }
    }
    return k;
}
int main(){
    int ans = 0;
    cin>>n>>r;
    for(int i = 1;i <= n;i++){
        cin>>t[i].v;
        t[i].avg = t[i].v;
        t[i].s = 1;
        ans += t[i].v;
    }
    
    for(int i = 1;i < n;i++){
        cin>>a>>b;
        t[b].f = a;
    }
    for(int i = 1;i < n;i++){
        int u = find(),f = t[u].f;
        t[u].avg = -1;
        ans += t[u].v * t[f].s;
        for(int j = 1;j <= n;j++){
            if(t[j].f == u) t[j].f = f;
        }
        t[f].v += t[u].v;
        t[f].s += t[u].s;
        t[f].avg = (double)t[f].v / t[f].s;
    }
    cout<<ans<<endl;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值