洛谷 P2726 [SHOI2005]树的双中心题解

题干

 

分析

首先我们看 min{d(v,x),d(v,y)} ,怎么分析呢?

  

我们假定 x 为2节点, y 为4节点,则显然对于1会去离它最近的2,同理3,5会去4。

那么整棵树就会被分为两部分,一块是12,另一块是345

那么对于任意的两个节点,都能将树分为两部分吗?

答案是肯定的,我们可以这么考虑:选择x,y后,整棵树可以变为(x及其子树,y及其子树,xy中间部分)

那么x的子树肯定会去x,y的肯定会去y,xy中间的部分则必然有一条线作为分割,一部分去x,一部分去y。

那么我们就考虑枚举从哪里将这一整棵树断开。

枚举每一条边,将其断开,会产生什么影响呢?

首先,是该节点以及其所有祖先的儿子会减少。

我们采用一个 weight_sum 数组,第i项表示i这个点的所有儿子和他自己的点权和。

那么我们考虑删去i这条边,则可以得出:

for(int p=father[i]; p; p=father[p]) weight_sum[p]-=weight_sum[i];

将这个点的所有祖先的点权和减去他的所有儿子以及它本身的点权和

之后我们要对这两部分分别进行处理。

对于每一个部分,我们要让该部分的 w(v)*d(k,v) 最小,也就是说,我们要求出这一部分的重心。

显然我们若采取 O(N) 的方法,也就是暴力的话,那么一定会超时,因为N其实很大。

那么我们就要考虑一个性质:

一棵树的重心一定在这棵树的根或者其重儿子中。

为什么呢?因为对于重心来说,去除这个点后,每部分的点权都一定小于总点权的一半。

那么如果在轻儿子上,根+重儿子+其他轻儿子的点权和必然超过了总点权的一半,否则轻儿子就成了重儿子了

所以我们只需要挑选当前树的最大子树,若 weight_sum[x]*2>total(子树权值和),则继续向下查找重心。

那么如果一条边被割断了,把最大的子树割出去了呢?那么剩下的部分的重心该如何处理呢?

很简单,我们只要再维护其次大子树,若最大的被割走了,择选择次大的即可。

我们将这些东西都预处理出来

于是就有了以下代码:

void get(int u, int v, int d) { 
    father[u]=v;
    weight_sum[u]=weights[u];
    depth[u]=d;
    for(int i=0; i<e[u].size(); i++) {
        int t=e[u][i];
        if(t==v) continue;
        get(t,u,d+1);
        weight_sum[u]+=weight_sum[t];
        dp[u]+=dp[t]+weight_sum[t];
        if(weight_sum[t] > maxsum[u]) {
            second_maxsum[u] = maxsum[u];
            max2[u] = max1[u];
            maxsum[u] = weight_sum[t];
            max1[u] = t;
        } 
    else 
        if(weight_sum[t] > second_maxsum[u]) {
            second_maxsum[u] = weight_sum[t];
            max2[u] = t;
        }
    }
}

father 数组用来记录当前点的父亲,每次没有搜索时 ,先把 weight_sum 定为一开始输入的 weight ,也就是它本身

depth 记录该点的深度

之后遍历该点的所有孩子,将孩子的点权和加到自己身上。

这里的dp表示所有点到当前点的权值和,显然, dp[u]=∑{dp[v]+weight_sum[v]*1} ,因为v到u之间的长度为1,所有的儿子到u的距离就增加了1

之后就是统计第一大和第二大的子树

maxsum和second_maxsum记录其权值和,max1,max2指向其对应的儿子。

然后我们回到处理这部分

我们处理的时候采用dfs,每次维护三个变量:当前节点,所有点到该点的权值和,以及一开始的权值总和。

先处理包含根节点的部分,我们假定根节点为1

每次进入 dfs 时,初始值分别为1,  dp[1]-dp[i]-weight_sum[i]*depth[i], weight_sum[1]

为什么是 dp1-dpi-weight_sumi*depthi 呢?

和上面很类似,就是先减掉所有i的子树到i的权值和,然后因为i到1的距离为depthi,所以每一部分还有额外减掉

depthi*weight_sumi

同理,我们处理割出去的部分时,初始值为i, dp[i], weight_sum[i],这不用过多解释

那么dfs怎么写呢?

     d=min(d,s);
    int t=max1[u];
    if(t==cut || weight_sum[t] < weight_sum[max2[u]]) t=max2[u];
    if(t && 2*weight_sum[t] > total) dfs(t, s+total-2*weight_sum[t], total);

d用于储存最小的点权和,最小时是求出重心的时候

cut是被切掉的边下面的那个点,如果最大子树指向这个点,或者切掉后他不再是最大子树,那么就采取备用子树

如果该点权值和大于总权值和的1/2,那么重心可能还在下面,就继续深搜

于是代码就完美的结束了,最后注意的是每次要把之前减掉的 weight_sum 还原~

主要部分:

void get(int u, int v, int d) { 
    father[u]=v;
    weight_sum[u]=weights[u];
    depth[u]=d;
    for(int i=0; i<e[u].size(); i++) {
        int t=e[u][i];
        if(t==v) continue;
        get(t,u,d+1);
        weight_sum[u]+=weight_sum[t];
        dp[u]+=dp[t]+weight_sum[t];
        if(weight_sum[t] > maxsum[u]) {
            second_maxsum[u] = maxsum[u];
            max2[u] = max1[u];
            maxsum[u] = weight_sum[t];
            max1[u] = t;
        } 
    else 
        if(weight_sum[t] > second_maxsum[u]) {
            second_maxsum[u] = weight_sum[t];
            max2[u] = t;
        }
    }
}

void dfs(int u, int s, int total) {
    d=min(d,s);
    int t=max1[u];
    if(t==cut || weight_sum[t] < weight_sum[max2[u]]) t=max2[u];
    if(t && 2*weight_sum[t] > total) dfs(t, s+total-2*weight_sum[t], total);
}
int main() {
    ios::sync_with_stdio(false);
    ans=0x3f3f3f3f;
    cin>>n;
    for(int i=1; i<n; i ++) {
        cin>>x>>y;
        e[x].push_back(y);
        e[y].push_back(x);
    }
    for(int i=1; i<=n; i++)  cin>>weights[i];
    get(1,0,0);
    for(int i=2; i<=n; i++) {
        pp=0;
        for(int p=father[i]; p; p=father[p]) weight_sum[p]-= weight_sum[i];
        d=0x3f3f3f3f;
        cut=i;
        dfs(1, dp[1]-dp[i]-weight_sum[i]*depth[i], weight_sum[1]);
        pp+=d;
        
        d=0x3f3f3f3f;
        dfs(i, dp[i], weight_sum[i]);
        pp+=d;
        ans=min(ans,pp);
        for(int p = father[i]; p; p = father[p]) weight_sum[p]+=weight_sum[i];
    }
View Code

 

感谢 Rosmarinus 同学提供的思路~

posted @ 2021-08-09 20:11  鹤翅拥三边  阅读(96)  评论(0)    收藏  举报