数据结构与算法:树的重心
前言
现在看这些题感觉格外善良,比cf那些抽象guess猜结论题好多了……
一、树的重心及性质
树的重心有以下三种定义,求出来的点是一样的:
(1)以某个节点为根时,最大子树的节点数最少,那么这个节点就是重心。
(2)以某个节点为根时,每棵子树的节点数不超过总节点数的一半,那么这个节点就是重心。
(3)以某个节点为根时,所有节点都走向该节点的总边数最少,那么这个节点就是重心。
补充的性质:
(4)一棵树最多有两个重心,如果有两个重心,那么两个重心一定相邻。
(5)如果在树上增加或删除一个叶节点,转移后的重心最多移动一条边。
(6)如果把两棵树连起来,那么新树的重心一定在原来两棵树重心的路径上。
(7)若树上的边权都为正数,不管边权如何分布,让所有节点都走向一个点时,走向重心的总距离和最小。
二、树的重心的求法
1.第一种——Balancing Act
poj这个评测真的依托……
第一种求解方式是基于树的重心的第一个定义,即最大子树的节点数最少。
#include<iostream>
#include<vector>
using namespace std;/* /\_/\
* (= ._.)
* / > \>
*/const int MAXN=5e4+5;vector<vector<int> >g(MAXN);//sz[u]:u的子树节点个数
vector<int>sz(MAXN);int n;//最大子树的节点数
int best;
//重心
int center;void dfs(int u,int fa)
{sz[u]=1;//以u为根节点时,最大的子树的节点个数int maxx=0;for(int i=0;i<g[u].size();i++){int v=g[u][i];if(v!=fa){dfs(v,u);sz[u]+=sz[v];maxx=max(maxx,sz[v]);}}//u上方部分的节点数maxx=max(maxx,n-sz[u]);//节点数更少或节点编号更小if(maxx<best||(maxx==best&&u<center)){best=maxx;center=u;}
}void build()
{for(int i=1;i<MAXN;i++){g[i].clear();sz[i]=0;}best=1e9;
}void solve()
{build();cin>>n;for(int i=0,u,v;i<n-1;i++){cin>>u>>v;g[u].push_back(v);g[v].push_back(u);}dfs(1,0);cout<<center<<" "<<best<<endl;
}signed main()
{ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);int t=1;cin>>t;while(t--){solve(); }return 0;
}
这个方法是不是只能求一个重心……
方法就是直接去dfs,统计子树大小。然后就需要求出以当前节点为重心时各个子树大小的最大值,那么就是先在所有孩子节点子树中取最大,然后父亲节点那部分的子树大小就是n减去当前节点的子树大小。若这个最大值能把全局最大值更新得更小,就直接更新当前树的重心。
2.第二种——树的重心
还是洛谷的评测好()
第二种求法是基于第二个定义,即每棵子树的节点数不超过总节点数的一半。
#include <bits/stdc++.h>
using namespace std;/* /\_/\
* (= ._.)
* / > \>
*/typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;const int MAXN=5e4+5;vector<vector<int>>g(MAXN);//sz[u]:u的子树节点个数
vector<int>sz(MAXN);//maxsub[u]:以u为根节点时,最大的子树的节点个数
vector<int>maxsub(MAXN);int n;void dfs(int u,int fa)
{sz[u]=1;//以u为根节点时,最大的子树的节点个数maxsub[u]=0;for(auto v:g[u]){if(v!=fa){dfs(v,u);sz[u]+=sz[v];maxsub[u]=max(maxsub[u],sz[v]);}}//u上方部分的节点数maxsub[u]=max(maxsub[u],n-sz[u]);
}void solve()
{cin>>n;for(int i=0,u,v;i<n-1;i++){cin>>u>>v;g[u].push_back(v);g[v].push_back(u);}dfs(1,0);vector<int>ans;for(int i=1;i<=n;i++){if(maxsub[i]<=n/2){ans.push_back(i);}}for(auto x:ans){cout<<x<<" ";}cout<<endl;
}signed main()
{ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);int t=1;//cin>>t;while(t--){solve(); }return 0;
}
这个方法就可以求出多个树的重心了。
这个题因为需要最后比较每个节点为重心时最大子树的个数,所以可以考虑定义maxsub数组,在dfs时把每个节点的最大子树大小存起来,最后再把小于等于n/2的节点都加入即可。
三、题目
1.Great Cow Gathering G
#include <bits/stdc++.h>
using namespace std;/* /\_/\
* (= ._.)
* / > \>
*/typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;const int MAXN=1e5+5;int n;
int sum;
vector<int>a(MAXN);
vector<vector<pii>>g(MAXN);int best=1e9;
int center;//sz[u]:从1节点开始dfs,u节点子树上牛的总数
vector<int>sz(MAXN);//path[u]:从重心开始dfs,到u节点的路程
vector<int>path(MAXN);void solve()
{cin>>n;for(int i=1;i<=n;i++){cin>>a[i];sum+=a[i];}for(int i=1,u,v,w;i<=n;i++){cin>>u>>v>>w;g[u].push_back({v,w});g[v].push_back({u,w});}//找重心auto dfs1=[&](auto &&self,int u,int fa)->void{sz[u]=a[u];int maxx=0;for(auto [v,w]:g[u]){if(v!=fa){self(self,v,u);sz[u]+=sz[v];maxx=max(maxx,sz[v]);}}maxx=max(maxx,sum-sz[u]);if(maxx<best){best=maxx;center=u;}};dfs1(dfs1,1,0);//设置路径长度auto dfs2=[&](auto &&self,int u,int fa)->void{for(auto [v,w]:g[u]){if(v!=fa){path[v]=path[u]+w;self(self,v,u);}}};path[center]=0;dfs2(dfs2,center,0);ll ans=0;for(int i=1;i<=n;i++){//让这个点的牛走过去ans+=1ll*a[i]*path[i];}cout<<ans<<endl;
}signed main()
{ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);int t=1;//cin>>t;while(t--){solve(); }return 0;
}
根据上述性质,肯定是让所有牛都聚集到树的重心上。那么就需要先dfs出树的重心,这里用的是第一种方法。在求出重心后,还需要再dfs一遍求出每个节点到重心的路径和,最后再遍历一遍让每个节点的牛走过去即可。
2.C. Link Cut Centroids
当年的题多么的善良……
#include <bits/stdc++.h>
using namespace std;/* /\_/\
* (= ._.)
* / > \>
*/typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;void solve()
{int n;cin>>n;vector<vector<int>>g(n+1);for(int i=0,u,v;i<n-1;i++){cin>>u>>v;g[u].push_back(v);g[v].push_back(u);}vector<int>maxsub(n+1);vector<int>sz(n+1);auto dfs=[&](auto &&self,int u,int fa)->void{sz[u]=1;for(auto v:g[u]){if(v!=fa){self(self,v,u);sz[u]+=sz[v];maxsub[u]=max(maxsub[u],sz[v]);}}maxsub[u]=max(maxsub[u],n-sz[u]);};dfs(dfs,1,0);vector<int>center;for(int i=1;i<=n;i++){if(maxsub[i]<=n/2){center.push_back(i);}}if(center.size()==1){cout<<1<<" "<<g[1][0]<<endl;cout<<1<<" "<<g[1][0]<<endl;return ;}int u=center[1];int fa=center[0];int ru=0;int rv=0;auto dfs2=[&](auto &&self,int u,int fa)->void{if(g[u].size()==1){if(ru==0){ru=fa;rv=u;}return ;}for(auto v:g[u]){if(v!=fa){self(self,v,u);}}};dfs2(dfs2,u,fa);cout<<ru<<" "<<rv<<endl;cout<<fa<<" "<<rv<<endl;
}signed main()
{ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);int t=1;cin>>t;while(t--){solve(); }return 0;
}
这个题的题意就很直白了,哪像现在的cf读题就得读半天,还得结合样例才能读懂……
首先肯定得求出树的重心,那么这里就得用第二种方法求了,若只有一个重心,那么直接随便找一条边断开再连上即可。而若有两个重心,那么根据重心的性质,这两个重心一定相连,且删除一个叶子后重心最多移动一条边。那么就可以考虑从一个重心开始,设置另一个重心为其父节点,然后删除一个这个重心子树上的叶节点,那么这个重心就会移动到父节点的那个重心上,最后再直接把这个节点连到唯一的重心上即可。
总结
加油加油!!
