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

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

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

一、(二叉)树的遍历

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

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

树相关算法(一)——二叉树的遍历、树的重心、树的直径_第1张图片

        这种遍历顺序和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)。

        比如对于下面这棵树。

树相关算法(一)——二叉树的遍历、树的重心、树的直径_第2张图片

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

树相关算法(一)——二叉树的遍历、树的重心、树的直径_第3张图片

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

#include
#include
#include
#include
using namespace std;
const int maxn=20100;
int n,father;
int siz[maxn];//siz保存每个节点的子树大小。
bool vist[maxn];
int CenterOfGravity=0x3f3f3f3f,minsum=-1;//minsum表示切掉重心后最大连通块的大小。
vectorG[maxn];
void DFS(int u,int x){//遍历到节点x,x的父亲是u。
	siz[x]=1;
	bool flag=true;
	for(int i=0;in/2) flag=false;//判断节点x是不是重心。
		}
	}
	if(n-siz[x]>n/2) flag=false;//判断节点x是不是重心。
	if(flag && x

三、树的直径

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

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

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

        下面给出证明。

        考虑这样一棵树,我们假设AB是树的直径,C的最远点为D,那么有AC0,所以a

        算法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
#include
#include
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  G[maxn];
void DFS(int x){
	f[x]=g[x]=0;
	for(int i=0;i

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

你可能感兴趣的:(算法&数据结构)