树相关算法(一)——二叉树的遍历、树的重心、树的直径

前言:算法竞赛中常见的树问题

  • (二叉)树的遍历
  • 树的重心
  • 树的直径
  • 最近公共祖先(LCA)
  • 哈夫曼树
  • 树链剖分

一、(二叉)树的遍历

        二叉树的遍历(Traversing binary tree)是指从根节点出发,按照某种次序一次访问二叉树中所有的节点,是的每个节点被依次访问且仅被访问一次。 

        我们规定一种遍历顺序为先访问中间的节点,在访问左边子树,当左面都访问完成以后再回来访问右面的子树。这样的话,对于下面这一棵树,我们访问节点的顺序应该是A->B->D->F->G->H->I->E->C。

        这种遍历顺序和DFS入栈的顺序很像,这种二叉树遍历方式称为先序遍历。除了先序遍历外,还有另外两种遍历,中序遍历和后序遍历。

        这三种遍历方式的特点归结如下:

  • 先序遍历:访问根节点,遍历左子树,遍历右子树;
  • 中序遍历:遍历左子树,访问根节点,遍历右子树;
  • 后序遍历:遍历左子树,遍历右子树,访问根节点。

        对于上面的那棵树,给出三种遍历方式是:

  • 先序遍历:A->B->D->F->G->H->I->E->C;
  • 中序遍历:F->D->H->G->I->B->E->A->C;
  • 后序遍历:F->H->I->G->D->E->B->C->A。

        说了这么多,二叉树的遍历有什么用呢?答案是,没太大用。一般情况下是用来当做题目中的信息。对于OIer来说这些是常识,初赛会考的。

二、树的重心

        树的重心,也叫树的质心。对于一棵树来说,删去该树的重心后,所有的子树的大小不会超过原树大小的二分之一。树的重心还有一个性质,是相对于树上的其他点而言的,就是删去重心后形成的所有子树中最大的一棵节点数最少。换句话说,就是删去重心后生成的多棵子树是最平衡的。一棵树的重心至多有两个。

        下面考虑重心的求法。我们考虑用第一个性质来求,这样比较简单。我们可以很容易的在一次DFS过程中求出所有节点的siz,即子树大小。我们每搜索完一个节点u的儿子v,就判断siz[v]是否大于n/2,然后在搜索完所有儿子后计算出本节点的siz,再判断n-siz[u]是否大于n/2(n-siz[u]是节点u上面的连通块大小)即可求出重心,时间复杂度O(n)。

        比如对于下面这棵树。

        我们任意选取一个节点作为根,将其转为有根树,假设我们了选取节点1。这棵树就会转成下面的样子。假设我们正在节点4处。我们需要判断删去节点4后,这棵树的任一子树大小是否会超过n/2,就是图中标出的三块。我们分别判断4的所有儿子(siz[5]和siz[6])是否大于n/2,再判断节点4上面的部分(n-siz[4])是否大于n/2,对于节点4,上述两个条件均被满足,那么节点4是这棵树的一个重心。同样地,我们可以求出节点2也是这棵树的重心。

        下面是代码实现(实际上是POJ1655Balancing Act的代码,DFS过程即可求出重心)。感谢Anonymous366提供代码,我在其基础上进行了修改,并增加了注释。

#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=20100;
int n,father;
int siz[maxn];//siz保存每个节点的子树大小。
bool vist[maxn];
int CenterOfGravity=0x3f3f3f3f,minsum=-1;//minsum表示切掉重心后最大连通块的大小。
vector<int>G[maxn];
void DFS(int u,int x){//遍历到节点x,x的父亲是u。
	siz[x]=1;
	bool flag=true;
	for(int i=0;i<G[x].size();i++){
		int v=G[x][i];
		if(!vist[v]){
			vist[v]=true;
			DFS(x,v);//访问子节点。
			siz[x]+=siz[v];//回溯计算本节点的siz
			if(siz[v]>n/2) flag=false;//判断节点x是不是重心。
		}
	}
	if(n-siz[x]>n/2) flag=false;//判断节点x是不是重心。
	if(flag && x<CenterOfGravity) CenterOfGravity=x,father=u;//这里写x<CenterOfGravity是因为本题中要求节点编号最小的重心。
}
void init(){
	memset(vist,false,sizeof(vist));
	memset(siz,0,sizeof(siz));
	minsum=-1;
	CenterOfGravity=0x3f3f3f3f;
	for(int i=0;i<maxn;i++) G[i].clear();
}
int main(){
	int T;
	scanf("%d",&T);
	while(T--){
		scanf("%d",&n);
		init();
		for(int i=1;i<n;i++){
			int u,v;
			scanf("%d%d",&u,&v);
			G[u].push_back(v);
			G[v].push_back(u);
		}
		vist[1]=1;
		DFS(-1,1);//任意选取节点作为根,根节点的父亲是-1。
		for(int i=0;i<G[CenterOfGravity].size();i++)
			if(G[CenterOfGravity][i]==father) minsum=max(minsum,n-siz[CenterOfGravity]);
			else minsum=max(minsum,siz[G[CenterOfGravity][i]]);
		printf("%d %d\n",CenterOfGravity,minsum);
	}
	return 0;
}

三、树的直径

        树的直径,即树上的最长路径,显然,树的直径可以有很多条(考虑一棵菊花)。

        接下来我们考虑如何求出一棵树的直径。有很多种O(n)的算法。

        算法1:我们任取树中的一个节点x,找出距离它最远的点y,那么点y就是这棵树中一条直径的一个端点。我们再从y出发,找出距离y最远的点就找到了一条直径。这个算法依赖于一个性质:对于树中的任一个点,距离它最远的点一定是树上一条直径的一个端点。

        下面给出证明。

        考虑这样一棵树,我们假设AB是树的直径,C的最远点为D,那么有AC<CD,a+c<d,因为c>0,所以a<d+c,故有a+b<b+c+d,AB<BD,与假设AB是直径矛盾,故性质得证。

        算法2:首先,先将无根树转成有根树,定义F[i]表示从i出发向远离根节点的方向走的最长路径的长度,G[i]表示从i向远离根节点的方向走的次长路径的长度。注意F[i]和G[i]不能沿着i的同一个儿子走。特别地,如果i只有一个儿子,那么G[i]=0。答案为max(F[i]+G[i])。

        下面是代码实现。再次感谢Anonymous366提供代码,我也进行了修改,并加了注释。这份代码可以求出带权树中的直径,如果只是一棵普通的树,那么val赋为1即可。

#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
const int maxn=10100;
int n,ans;
int f[maxn],g[maxn];//f表示最长路,g表示次长路。
bool vist[maxn];
struct Node{
	int to,val;
	Node(int to=0,int val=0):to(to),val(val){}
};
vector <Node> G[maxn];
void DFS(int x){
	f[x]=g[x]=0;
	for(int i=0;i<G[x].size();i++){
		Node v=G[x][i];
		if (!vist[v.to]){
			vist[v.to]=true;
			DFS(v.to);//访问子节点。
			vist[v.to]=false;
			if (f[x]<f[v.to]+v.val){//如果发现了一条更长的路径,那么更新f[x]和g[x]。
				g[x]=f[x];//原来的f[x]变为次长路,新发现的记为最长路。
				f[x]=f[v.to]+v.val;
			}
			else if (g[x]<f[v.to]+v.val) g[x]=f[v.to]+v.val;//如果找到了一条比次长路更长的路径,那么更新g[x]。
		}
	}
	ans=max(ans,f[x]+g[x]);
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<n;i++){
		int u,v,val;
		scanf("%d%d%d",&u,&v,&val);
		G[u].push_back(Node(v,val));
		G[v].push_back(Node(u,val));
	}
	vist[1]=true;
	DFS(1);
	printf("%d\n",ans);
	return 0;
}    

        这次的讲解就到这里,其它的问题会在后续博客中更新。

猜你喜欢

转载自blog.csdn.net/zhanxufeng/article/details/80715185
今日推荐