定义及性质
定义1:找到一个点,删除它得到的森林中最大的子树节点数最少,那么这个点就是这棵树的重心。
定义2:删除重心后得到的所有子树,其顶点树必然不超过n/2
性质1:树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么他们的距离和一样。
性质2:把两个树通过一条边相连得到一个新的树,那么新的树的重心在连接原来两个树的重心的路径上。
性质3:把一个树添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
求法
根据定义,求解重心只需比较分别删除每一个结点后形成的森林中最大子树的结点数,该指标最小的结点即为树的重心。删除某一结点x得到的子树可以分成两类,一类x的子树,另一类是整棵树中除去以x为根的子树的部分。计算第一类的数量只需知道x的儿子有多少个子孙,计算第二类的数量只需知道x有多少个子孙。
因此核心问题就是知道这棵树中每一个结点有多少个子孙,这就是一个树型DP的问题,若son[i]表示i号结点的子孙数(不包含自己),则
其中j为i的儿子。
只需DFS一遍这棵树就可以求出son[].
例1:POJ 1655 Balancing Act
题目大意:给定一棵树,求树的重心的编号以及重心删除后得到的最大子树的节点个数size,如果size相同就选取编号最小的.
#include <iostream>
#include <cstdio>
#include <cstring>
const int INF=0x3f3f3f3f;
const int MAXN=20010;
const int MAXM=2*MAXN;
using namespace std;
struct Edge
{
int to,next;
}e[MAXM];
int n,head[MAXN],edgenum;
int son[MAXN],Size,num;
bool vis[MAXN];
void Add_Edge(int u,int v)
{
e[++edgenum].to=v;
e[edgenum].next=head[u];
head[u]=edgenum;
}
void dfs(int u)
{
vis[u]=true;
int temp=0;//temp维护删除u结点后形成的最大子树结点数
for (int t=head[u];t!=-1;t=e[t].next)
{
int v=e[t].to;
if (!vis[v])
{
dfs(v);
son[u]+=son[v]+1;//树型DP
temp=max(temp,son[v]+1);
}
}
temp=max(temp,n-son[u]-1);//整棵树除去以u为根的子树的部分
if (temp<Size||(temp==Size&&u<num))
{
Size=temp;//Size维护删除某一结点形成最大子树结点数的最小值
num=u;//num维护重心编号
}
}
int main()
{
int T,i,u,v;
scanf("%d",&T);
while (T--)
{
scanf("%d",&n);
edgenum=0;
memset(head,-1,sizeof(head));
memset(vis,false,sizeof(vis));
memset(son,0,sizeof(son));
for (i=1;i<n;i++)
{
scanf("%d%d",&u,&v);
Add_Edge(u,v);
Add_Edge(v,u);
}
Size=INF;num=0;
dfs(1);
printf("%d %d\n",num,Size);
}
return 0;
}
例2:POJ 3107 Godfather
题目大意:给定一棵树,求树的所有重心,按照编号从小到大的顺序输出.
分析:与例1不一样的地方只需每更新到一个最小值的时候就记录其它的最小值也为这个最小值的重心,这样下去就会找到所有的重心。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
const int MAXN=50010;
const int MAXM=MAXN*2;
const int INF=0x3f3f3f3f;
using namespace std;
struct Edge
{
int to,next;
}e[MAXM];
int n,edgenum,head[MAXN];
int son[MAXN],ans[MAXN],Size,cnt;
bool vis[MAXN];
void Add_Edge(int u,int v)
{
e[++edgenum].to=v;
e[edgenum].next=head[u];
head[u]=edgenum;
}
void dfs(int u)
{
vis[u]=true;
int temp=0;
for (int t=head[u];t!=-1;t=e[t].next)
{
int v=e[t].to;
if (!vis[v])
{
dfs(v);
son[u]+=son[v]+1;
temp=max(temp,son[v]+1);
}
}
temp=max(temp,n-son[u]-1);
if (temp<Size)
{
cnt=1;
Size=temp;
ans[cnt]=u;
}
else if (temp==Size)//若一样,把它记录到ans数组中
ans[++cnt]=u;
}
int main()
{
int i,u,v;
scanf("%d",&n);
edgenum=0;
memset(head,-1,sizeof(head));
for (i=1;i<n;i++)
{
scanf("%d%d",&u,&v);
Add_Edge(u,v);
Add_Edge(v,u);
}
Size=INF;cnt=0;
memset(son,0,sizeof(son));
memset(vis,false,sizeof(vis));
dfs(1);
sort(ans+1,ans+cnt+1);//按编号升序输出
for (i=1;i<cnt;i++)
printf("%d ",ans[i]);
printf("%d\n",ans[cnt]);
return 0;
}
应用
树的重心可以应用于树的分治中。
由于删除重心后得到的所有子树,其顶点数必然不要过n/2。因此如果每次选择重心进行分割,那么每次树的大小也至少减半,所以递归深度是O(log n),可以避免O(
n2
)的极端复杂度。