题目链接

https://www.qduoj.com/problem/80

题目大意

n个节点,相连节点距离为1,顺序输出每个节点距离其他节点的距离之和。
举个例子:
图片说明
假如构造的树是这样的,因为要按照节点序号输出,所以我们先以1号节点为根节点。
那输出的结果应该为

15 //15 = (9->1 = 1) + (6->1 = 1) + (5->1 = 2) + (3->1 = 2) + (4->1 = 2) + (8->1 = 2) + (2->1 = 2) + (7->1 = 3)
25 //25 = (6->2 = 1) + (8->2 = 2) + (1->2 = 2) + (9->2 = 3) + (5->2 = 4) + (3->2 = 4) + (4->2 = 4) + (7->2 = 5)
19 //……
21
21
18
26
25
14

解题思路

两次深搜。

第一次深搜

变量介绍

参数x表示当前节点,参数fa表示其父节点,局部变量p表示x的子节点;
cnt[i]表示以i为根节点的子树上(包含根节点)节点的数量(在第一次深搜里,cnt数组的含义暂时是这样的);
sum[i]表示以i为根节点的子树上所有点到i节点的距离和(在第一次深搜里,sum数组的含义暂时是这样的)

深搜目的

对cnt数组和sum数组进行初步赋值,含义正如上述。

深搜实现

以1节点为根节点开始深搜,不停地访问其子节点;
因为我们的目的是对cnt数组和sum数组初始化,它们(暂时)的含义分别是“以i为节点的子树的节点个数”和“以i为节点的子树中所有节点到根节点的距离和”,因此,我们想要对每个子树的根节点的cnt和sum赋值的话,就需要先把其子节点赋上值,才能算出根节点的cnt和sum值。自然,我们要先dfs到底,再通过递归返回进行赋值与加和,实现对cnt数组和sum数组的赋值;

//核心赋值语句:
cnt[x]+=cnt[p];//对cnt赋值
sum[x]+=sum[p]+cnt[p]*1;//对sum赋值

已知p是x的子节点,cnt[x]代表以x为根节点的子树的节点数,它加上所有以x子节点为根节点的子树的全部节点就是以x为根节点子树的节点个数;sum[x]表示以x为根节点的子树上所有点到x节点的距离和,它加上所有以x子节点为根节点的子树的sum值,再加上以x子节点为根节点的子树的节点个数与距离的乘积,就得到了sum[x]。(语言表达太难了,直接上图!)
注意:cnt要初始化为1,因为每个点为根节点的子树,至少有自己本身这个节点,因此初始化为1;而本节点到本节点的距离为0,因此sum初始化为0。
还是上图中的树,

//cnt的赋值
cnt[1] = cnt[9] + cnt[6] + 1 //初始化的1
cnt[9] = cnt[5] + cnt[3] + cnt[4] + 1
cnt[3] = cnt[7] + 1
……

//sum的赋值
sum[1] = (sum[9] + cnt[9]*1) + (sum[6] + cnt[6]*1) //1是从9节点到1节点的距离,也是从6节点到1节点的距离
sum[9] = (sum[5] + cnt[5]*1) + (sum[3] + cnt[3]*1) + (sum[4] + cnt[4]*1)
sum[3] = (sum[7] + cnt[7]*1)

cnt数组的赋值比较好理解,但是sum数组的赋值相对不好理解一些,看图吧!

图片说明
可以理解为每个节点上都站着一个人,节点序号就是人的序号,计算sum[1]就是计算第2,3,4,5,6,7,8,9号人到1号人位置的距离和(走过的路程和),这就可以转化为绿框里的人先走到第9号人的位置,再和第9号人一起走到第1号人的位置,紫框里的人先走到第6号人的位置,再和第6号人一起走到第1号人的位置,这两部分和距离正是sum[1]。转换成代码,绿框里的人走到第9号人的位置的距离是sum[9],紫框里的人走到第6号人的位置的距离是sum[6],此时,绿框里的人已经与第9号人汇合了,紫框里的人已经与第6号人汇合了,然后红框里的人再一起从第9号人的位置出发,前往第1号人的位置,体现在代码上为cnt[9] * 1,含义为红框里的所有人前往第1号人的位置,蓝框里的人也一起从第6号人的位置出发,前往第1号人的位置,体现在代码上为cnt[6] * 1,含义为蓝框里的所有人前往第1号人的位置。综上,sum[1] = (sum[9] + cnt[9] * 1) + (sum[6] + cnt[6] * 1),转移方程为sum[根节点] = 图片说明 (sum[子节点]+cnt[子节点] * 1)

第二次深搜

变量介绍

参数x表示当前节点,参数fa表示其父节点,局部变量p表示x的子节点;(同上)
cnt[i]在被更新前,表示以i为根节点的子树上(包含根节点)节点的数量(同上);在被更新后,表示为以i节点为整棵树的根节点,所含节点的个数,即全部节点个数n;
sum[i]在被更新前,表示以i为根节点的子树上所有点到i节点的距离和(同上);在被更新后,表示为全部节点到i节点的距离和。

深搜目的

这次深搜是为了刷新sum值,因为最终输出的是sum值。
cnt值的更新是为了刷新sum值才进行的。

深搜实现

以1节点为根节点开始深搜,不停地访问其子节点;
我们的目的是更新cnt数组和sum数组,它们(更新后)的含义分别是“表示为以i节点为整棵树的根节点,所含节点的个数”和“全部节点到i节点的距离和”。要更新的是p节点的sum和cnt值,而更新p节点的sum和cnt值需要利用x节点的sum和cnt值,因此,需要先更新完根节点的sum和cnt值,才能去更新子节点的sum和cnt的值。自然,我们要先更新sum和int,再dfs子节点;
到这里,你会发现(你应该没发现)第一个深搜每次的操作对象是x节点,而第二个深搜每次更新的对象是p节点,即x的子节点。
那么,为什么要更新的是p节点的值而不是x节点的值呢???
从第二次深搜开始说起吧,从1节点dfs进入,此时未更新的sum[1]表示的就是最终的sum[1],也就是全部节点到1节点的距离,因为1节点是整棵树的根节点,它现在的sum值就是它最终的sum值。按照dfs的思想,我们每次知道的都是根节点最终的sum值,那就用这个最终值去更新其子节点的sum值。
每次更新完sum值之后,都要把子节点对应的cnt值赋值为其父节点的cnt值,即n,因为每一次dfs都是从1节点进入的,所以每一次cnt都被传递性地赋值为了n。这样做的目的是,把p节点当成整棵树的根节点,方便p的子节点sum值的更新。(看看转移方程就明白了)

//核心更新语句
sum[p]+=(sum[x]-sum[p]-cnt[p]*1)+(cnt[x]-cnt[p])*1;
cnt[p]=cnt[x];

好长好麻烦的转移方程
还是以上图为例,
图片说明
刚进入dfs时,p为第9号人的位置,x为第1号人的位置,fa=0不对应任何节点,欲求sum[p],即全部人到第9号人位置的路程和,就需要用红框中人到第9号人位置的路程和加上蓝框中人到第9号人位置的路程和。其中,红框里的人到第9号人位置的距离和就是未更新时的sum值,因为红框刚好是9节点的子树除去根节点的部分;
而蓝框里的人到第9号人位置的路程和求起来就比较麻烦了,现在已知的条件是未更新的所有节点的cnt值和为最终答案的sum[1],那我们就通过这些条件来求。sum[1] = 紫框里所有人走到第1号人位置的路程和 + 绿框里所有人走到第1号人位置的路程和,进一步分解,sum[1] = 红框里所有人走到第9号人位置的路程和 + 汇合在第9号人位置的所有人一起去第1号人位置的路程和 + 绿框里所有人走到第1号人位置的路程和,代码表示为,sum[1] = sum[9] + cnt[9] * 1 + 绿框里所有人到第1号人的路程和,进一步转化,绿框里所有人到第1号人的路程和 = sum[1] - sum[9] - cnt[9] * 1。见证奇迹的时刻,蓝框所有人到第9号人位置的路程和 = 绿框所有人到第1号人位置的路程和 + 汇合在第1号人位置的所有人一起去第9号人位置的路程和,把上述转化得到的方程代入得,蓝框所有人到第9号人位置的路程和 = (sum[1] - sum[9] - cnt[9] * 1) + (cnt[1] - cnt[9]) * 1。
因此,最终sum[9] = sum[9] + (sum[1] - sum[9] - cnt[9] * 1) + (cnt[1] - cnt[9]) * 1;
=>sum[9] += (sum[1] - sum[9] - cnt[9] * 1) + (cnt[1] - cnt[9])*1;
cnt的更新显得简单多了,就是重新换一个树根。
通过特例的讲解理解过程,接着再说一下一般形式,
图片说明
整棵树被分成了4部分,p节点,x节点,与p相连部分,与x相连部分,且4部分相互独立共同构成整棵树。本质上,x与p不具有父子关系,但是sum和cnt对应的x和p存在父子关系,我们要做的就是通过cnt和sum求出最终的sum,其实也就是利用左红框区域与右红框区域的cnt与sum的关系计算出p节点的最终sum值,与树的形状无关。

AC代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+10;
vector<int> a[N];
ll sum[N],cnt[N];
void dfs(int x,int fa){//初步构建sum和cnt数组 
    cnt[x]=1,sum[x]=0;
    for(int i=0;i<a[x].size();i++){
        int p=a[x][i];
        if(p==fa) continue;
        dfs(p,x);
        cnt[x]+=cnt[p];
        sum[x]+=sum[p]+cnt[p]*1;    
    }
}
void dfs1(int x,int fa){
    for(int i=0;i<a[x].size();i++){
        int p=a[x][i];
        if(p==fa) continue;
        sum[p]+=(sum[x]-sum[p]-cnt[p]*1)+(cnt[x]-cnt[p])*1;
        cnt[p]=cnt[x];
        dfs1(p,x);
    }
}

int main(){
    int n;
    cin>>n;
    memset(sum,0,sizeof sum);
    memset(cnt,0,sizeof cnt);
    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        a[u].push_back(v);
        a[v].push_back(u);
    }
    dfs(1,0);
    dfs1(1,0);
    for(int i=1;i<=n;i++)
        cout<<sum[i]<<endl;    
}

我的错误代码

//我感觉挺对的,就是数组开不下,其实还是错的。。。。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int INF=99999999;
const int N=1e3+5;//这里开不下
int dis[N][N];
vector<int> e[N];
int dfs(int x,int y,int fa){
    if(x==y) return dis[x][y]=0;
    if(dis[x][y]!=INF) return dis[x][y];
    for(int i=0;i<e[x].size();i++){
        dis[x][e[x][i]]=1;
        if(e[x][i]!=fa){
            dis[x][y]=min(dis[x][y],dis[x][e[x][i]]+dfs(e[x][i],y,x));
            dis[y][x]=dis[x][y];
        }        
    }
    return dis[x][y];
}
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            dis[i][j]=INF;
    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        e[u].push_back(v);
        e[v].push_back(u);
    }
    for(int i=1;i<=n;i++){
        ll sum=0;
        for(int j=1;j<=n;j++){
            dis[i][j]=dfs(i,j,0);
            sum+=dis[i][j];    
        }    
        cout<<sum<<endl;
    }        
}

总结

又是一道简单的难题,啥意思?就是说这个题可以变形成难题,但是基本思路不变。
我太菜了,根本不会,一个没过多久就会忘掉这种做法。
常回来看看,才能加深印象。
最后的最后,我还想再问一句:我什么时候才能成为大佬啊啊啊!!!