cf1466 GoodBye2020-D 【图论+贪心】

Date:2021.12.23

题意:给出一个由n个点构成的树,每个点都有一个权值。现在你可以用k,k⊂[1,n]个颜色来给这棵树上的边涂色(这k种颜色不一定都要用上)。对于每种颜色都有一个权重,权值定义如下:
将除了当前颜色coli其他颜色的边删掉,剩余的边构成了一个个联通分量。对于任意一个联通分量我们设它的权重是 w i w_i wi,那么 w i w_i wi就等于该联通分量中所有点的权值之和,则对于当前颜色,它的权值就是 w c o l i w_{coli} wcoli=max{ w 1 w_1 w1, w 2 w_2 w2,⋯, w n w_n wn} 需要注意的是,这个联通分量是通过删边得到的,所以不会出现联通分量中只有一个点的情况。对于一个空的联通分量,我们认为它的权重为0。
现在又定义对于每个k也有一个权值,它等于它包含的所有颜色的权值之和,即 w k w_k wk= ∑ i = 0 k \sum_{i=0}^k i=0k w c o l i w_{coli} wcoli.
求出对于每个k,它的权值 w k w_k wk的最大值是多少。

首先是我的大体思路,已经想明白了为什么不对。
思路①:我们不妨模拟一下染色的过程,不难发现每一次染色都相当于将一个点的权值贡献到答案里去,因此求每一步的最大答案无疑贪心。但是此时存在一个问题,并非每个点都是能对答案有贡献的,比如开始度为1的点,不论如何染色始终只有在k=1时贡献一次,此后再也不贡献了,因此我们先将度为0的点对应权值置为-1(权值可能存在0),其它点每个都可以至少取一次。按输入的边中两个点的最大值排序(如果最大值相同排最小值大的排,见cmp),再枚举每条边的两个点,每次取大的点的权值,依次取便可实现贪心。
代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
LL n,m,k,t;
const int N = 2e5+10;
struct node
{
    int x,y;
    LL sum=0;
}s[N];
int d[N],w[N];
bool cmp(node a,node b)
{
    if(max(w[a.x],w[a.y])==max(w[b.x],w[b.y]))
        return min(w[a.x],w[a.y])>min(w[b.x],w[b.y]);
    else return max(w[a.x],w[a.y])>max(w[b.x],w[b.y]);
}
int main()
{
    cin>>t;
    while(t--)
    {
        LL sums=0;
        memset(d,0,sizeof d);
        cin>>n;
        for(int i=1;i<=n;i++) 
        {
            cin>>w[i];
            sums+=w[i];
        }
        for(int i=1;i<n;i++)
        {
            cin>>s[i].x>>s[i].y;
            //s[i].sum=w[s[i].x]+w[s[i].y];
            d[s[i].x]++;d[s[i].y]++;
        }
        
        for(int i=1;i<n;i++)
        {    
            if(d[s[i].x]==1) w[s[i].x]=-1;
            if(d[s[i].y]==1) w[s[i].y]=-1;
        }
        sort(s+1,s+n,cmp);
        cout<<sums<<' ';
        if(n==2) 
        {
            cout<<endl;
            continue;
        }
        //for(int i=1;i<n;i++) cout<<"W:"<<w[s[i].x]<<' '<<w[s[i].y]<<endl;
        for(int i=2;i<=n-1;i++)
        {
            sums+=max(w[s[i].x],w[s[i].y]);
            cout<<sums<<' ';
        }
        cout<<endl;
    }
    return 0;
}

然而wa2麻了,想了半天找到了问题,问题就在这里。因为共输出n-1个数,k=1时结果固定,我脑子一热输出k>=2时的情况在这里写了个i=2,正好凑出了样例1,很奇怪。但是这里其实不论写i=1~n-2 、2~n-1,写多少都是不对的,为什么?

for(int i=2;i<=n-1;i++)
{
    sums+=max(w[s[i].x],w[s[i].y]);
    cout<<sums<<' ';
}

首先,我们忽略了一个重要大前提—同一颜色的边和所连结点一定在一整个连通块中。即不存在同一颜色且不连通的情况。原理很简单:
“对于任意一个联通分量我们设它的权重是 w i w_i wi,那么 w i w_i wi就等于该联通分量中所有点的权值之和,则对于当前颜色,它的权值就是 w c o l i w_{coli} wcoli=max{ w 1 w_1 w1, w 2 w_2 w2,⋯, w n w_n wn}”
由此不难看出,该连通块本来连通,因此权值是每个结点的和;如果给同一颜色的连通块“拦腰斩断”,斩断之后整个颜色的权值为那个最大权值的连通分量,权值必然变小。
换言之,即使存在“拦腰斩断”的情况,将两部分中的一部分转化为这种颜色则转化为每个颜色只存在于一个连通块中的情况,且所得解显然更优,画个图则不难理解。在这里插入图片描述由此引出正确题解。
思路②:贡献的本质是点(因此无需费事来找边上两个点哪个权值较大),给每个点都标记一个度,每个点能用的次数不能超过度的大小。注意k=1时,每个节点都被算进去了一次,因此每个节点的度都应先减1,由此开始对节点的贪心而不是对边的贪心,从而避免添加某一颜色时将别的颜色“拦腰斩断”的情况。
代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
LL n,m,k,t;
const int N = 2e5+10;
struct node
{
    int d,w;
}s[N];
bool cmp(node a,node b)
{
    if(a.w==b.w) return a.d>b.d;
    else return a.w>b.w;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>t;
    while(t--)
    {
        LL sums=0;
        cin>>n;
        for(int i=1;i<=n;i++) 
        {
            cin>>s[i].w;
            sums+=s[i].w;
            s[i].d=0;
        }
        for(int i=1;i<n;i++)
        {
            int x,y;cin>>x>>y;
            s[x].d++;s[y].d++;
        }
        for(int i=1;i<=n;i++) s[i].d--;
        sort(s+1,s+1+n,cmp);
        cout<<sums<<' ';
        if(n==2) 
        {
            cout<<endl;
            continue;
        }
        //for(int i=1;i<n;i++) cout<<"W:"<<w[s[i].x]<<' '<<w[s[i].y]<<endl;
        int ans=1;
        for(int i=1;i<=n;i++)
        //开始有些纠结,但是这里不会超时,看似是n^2其实树中所有结点的度最大为2*(n-1),因此整个遍历次数不会超过n-1,且正好是n-1
        {
            for(int j=1;j<=s[i].d;j++)
            {
                sums+=s[i].w;
                cout<<sums<<' ';
            }
        }
        cout<<endl;
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值