2020 CCPC-Wannafly Winter Camp Day3 G-火山哥周游世界(树形dp)

题目链接:G-火山哥周游世界
题意:一颗n个点的无根带权树,有k个特殊点,可以走到任一个点停止,现在要让你求对于每个点i作为起始点,走遍所有特殊点所花的最小路径和, n < = 5 e 5 n<=5e5 n<=5e5

先考虑若要从起点出发,最后回到起点所需要的路径和,这个是到所有特殊点的所有边权加起来乘2,去和回。

若不回到起点选择某个特殊点作为终点停止,则是上述路径和减去从起点到该特殊点的路径和,故要让结果最小,必然是减去一个距离起点最远的特殊点的路径,用sum表示经过的所有边权和,len[x]表示从起点到特殊点的距离,那么在确定了起点的情况下可以得到一个这样的式子: s u m ∗ 2 − m a x ( x ∈ [ 特 殊 点 ] , l e n [ x ] ) sum*2-max(x \in [特殊点] ,len[x]) sum2max(x[],len[x])

也就是说只要算出来每个点作为根时的sum与那个max则可以解决该题了。

于是考虑二次扫描,第一次先算出1作为根时的答案,一次dfs即可算出。

第二次dfs时算每个点的sum与max。

  1. 先看sum,对于一个点x,有两部分组成,一个是x向下延展,另一个是通过父亲节点走向其他特殊点,此时只需要判断是否需要向下(下方有特殊点),以及是否需要向上即可。
  2. 再看max,max也是来自两部分,向下的和通过父亲节点出去的,这里需要在第一次dfs时维护路径最大值与次大值,道理在这里:树的中心

做完了。

#include<bits/stdc++.h>
using namespace std;

typedef long long ll;

const int maxn=5e5+7;
struct Edge{
    int v,w,next;
}edge[maxn<<1];

int head[maxn],top;
void init(){
    top=0;
    memset(head,-1,sizeof(head));
}

void add(int u,int v,int w){
    edge[top].v=v;
    edge[top].w=w;
    edge[top].next=head[u];
    head[u]=top++;
}

ll siz[maxn]; //以1为根,x的子树中国家数量;
ll sum[maxn]; //以1为根,x的子树中到国家并返回的路径和;
ll res[maxn]; //以x为根,到所有国家并返回的路径和;

ll Fmax[maxn]; //以1为根,到子树的最大值;
ll Smax[maxn]; //以1为根,到子树的次大值;
ll neber[maxn]; //以1为根时,最大值那个节点;

ll Up[maxn]; //以1为根,x通过父亲走到国家的最大值;

bool f[maxn]; // 国家;

int n,k;
void dfs(int u,int fa){
    if(f[u]) siz[u]=1;
    for(int i=head[u];i!=-1;i=edge[i].next){
        int v=edge[i].v,w=edge[i].w;
        if(v==fa) continue;
        dfs(v,u);
        siz[u]+=siz[v];
        sum[u]+=sum[v];
        if(siz[v]){
            sum[u]+=2*w;
            if(Fmax[v]+w>=Fmax[u]){
                Smax[u]=Fmax[u];
                Fmax[u]=Fmax[v]+w;
                neber[u]=v;
            }
            else{
                Smax[u]=max(Smax[u],Fmax[v]+w);
            }
        }
    }
}

ll ans[maxn];

void dfs2(int u,int fa){
    for(int i=head[u];i!=-1;i=edge[i].next){
        int v=edge[i].v,w=edge[i].w;
        if(v==fa) continue;
        if(k-siz[v]){
            if(neber[u]==v) Up[v]=max(Up[u],Smax[u])+w;
            else Up[v]=max(Up[u],Fmax[u])+w;
        }
        res[v]=res[u]-sum[v];
        if(siz[v]) res[v]-=w*2;
        res[v]+=sum[v];
        if(k-siz[v]) res[v]+=2*w;
        //res[v]=res[u]-sum[v]-siz[v]*w+(k-siz[v])*w+sum[v];
        dfs2(v,u);
    }
}

int main(){
    int u,v,w;
    scanf("%d%d",&n,&k);
    init();
    for(int i=1;i<n;++i){
        scanf("%d%d%d",&u,&v,&w);
        add(u,v,w),add(v,u,w);
    }
    for(int i=1;i<=k;++i) scanf("%d",&u),f[u]=1;
    dfs(1,0);
    //for(int i=1;i<=n;++i) cout<<Fmax[i]<<" "<<Smax[i]<<" "<<neber[i]<<" "<<sum[i]<<endl;
    res[1]=sum[1];
    dfs2(1,0);
    //for(int i=1;i<=n;++i) cout<<res[i]<<endl;
    for(int i=1;i<=n;++i) ans[i]=res[i]-max(Up[i],Fmax[i]);
    for(int i=1;i<=n;++i) printf("%lld\n",ans[i]);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值