树与图的遍历【总结】

铺垫

树与图最常见的存储方式就是邻接表。树可以看作是具有 N − 1 N-1 N1条边的无向图,他们的边都存储在一个邻接表中,邻接表以 h e a d head head数组为表头,使用 v e r ver ver e d g e edge edge数组分别存储边的终点和权值,使用 n e x t next next数组模拟链表中的指针。

树与图的深度优先遍历

深度优先遍历,就是就是在某一个节点 x x x有多条分支时,任选一条边走下去,进行递归,直到回溯到 x x x,再考虑走另一条边,根据上面提到的存储方式,我们可以采用下面的代码,调用 d f s ( 1 ) dfs(1) dfs(1),对一张图进行深度优先遍历。
C o d e : Code: Code:

void dfs(int x)
{
	vis[x] = 1;//记录x被访问过
	for (int i = head[x]; i; i = nxt[i])//用next编译会报错
	{
		int y = ver[i];//边的终点
		if (vis[y]) continue;//如果访问过,则跳转到下一个
		dfs(y);//递归遍历
	}
}

这段代码访问每个节点恰好1次(无向边正反各一次),时间复杂度为 O ( N + M ) O(N+M) O(N+M), M M M为边数。

树的DFS序

我们在对树进行深度优先遍历时,对于每个节点,在刚进入递归后以及即将回溯时,各记录一次该点的编号,最后产生的 2 N 2N 2N的节点序列(递归前+回溯前)即称为树的 d f s dfs dfs

C o d e : Code: Code:

void dfs(int x)
{
    a[++m] = x;//a数组存储dfs序
	vis[x] = 1;//记录x被访问过
	for (int i = head[x]; i; i = nxt[i])//用next编译会报错
	{
		int y = ver[i];//边的终点
		if (vis[y]) continue;//如果访问过,则跳转到下一个
		dfs(y);//递归遍历
	}
	a[++m] = x;
}

d f s dfs dfs序的特点是:每个节点 x x x的编号在序列中恰好出现两次。设这两次出现的位置为 L [ x ] L[x] L[x] R [ x ] R[x] R[x],那么闭区间 [ l [ x ] , R [ x ] ] [l[x],R[x]] [l[x],R[x]]就是以 x x x为根的子树的 d f s dfs dfs序(因为在这个子树中第一个递归的是 x x x,最后一个回溯的也是 x x x).所以,可以通过 d f s dfs dfs序把树转化区间进行解题。

树的深度

树中各个节点的深度是一种自顶向下的统计信息。起初,我们已知根节点的深度为 0 0 0,若节点 x x x的深度是 d e e p [ x ] deep[x] deep[x],则它的子结点y的深度就是 d e e p [ y ] = d e e p [ x ] + 1 deep[y]=deep[x]+1 deep[y]=deep[x]+1.在深度优先遍历的过程中结合自顶向下的递推,就可以求出每一个节点的深度d。

C o d e : Code: Code:

void dfs(int x)
{
	vis[x] = 1;
	for (int i = head[x]; i; i = nxt[i])
	{
		int y = ver[i];
		if (vis[y]) continue;
		deep[y]=deep[x] + 1; //从父结点x到子节点y进行递推,计算深度
		dfs(y);
	}
}

树的重心

也有许多信息是自底向上统计的,比如以每个节点 x x x为根的子树大小 s i z e [ x ] size[x] size[x].对于叶子节点,我们知道:以这个叶子节点为根的子树大小为 1 1 1,若节点 x x x k k k个字节点 Y 1 Y_1 Y1 ~ Y k Y_k Yk,并且以 Y 1 Y_1 Y1~ Y k Y_k Yk为根的子树大小分别是 s i z e [ x ] = s i z e [ Y 1 ] + s i z e [ Y 2 ] + . . . + s i z e [ Y k + ] + 1 size[x]=size[Y_1]+size[Y_2]+...+size[Y_k+]+1 size[x]=size[Y1]+size[Y2]+...+size[Yk+]+1
对于一个节点x,如果我们把它从树中删除,那么原来的一棵树可能会分成若干个不相连的部分,其中每部分都是一棵子树。用 M a x N u m ( x ) MaxNum(x) MaxNum(x)表示删除x节点后产生的子树中最大一棵的大小。使 M a x N u m ( x ) MaxNum(x) MaxNum(x)函数取到最小值的节点p就称为整棵树的重心。

C o d e : Code: Code:

void dfs(int x)
{
	vis[x] = 1;size[x] = 1;//子树x的大小 
	int max_num = 0;//删掉x后分成的最大子树的大小 
	for (int i = head[x]; i; i = nxt[i])
	{
		int y = ver[i];
		if (vis[y]) continue;
		dfs(y);
		size[x] += size[y];//从子节点向父节点递推 
		max_num = max(max_num, size[y]);
	}
	max_num = max(max_num, n - size[x]);//n为整棵树的节点数目
	if (max_num < ans)
	{
		ans = max_num;//ans是全局变量记录重心所对应的max_num值 
		pos = x;//pos是全局变量记录重心 
	} 
}

连通块的划分

上面的代码每从x开始一次遍历,就会访问x能够到达得所有节点与边。所以我们可用使用多次dfs,划分出一张图中的各个连通区域。同理,对一个森林进行dfs可以划分出森林中的每棵树,如下面代码,cnt是图包含的连通区域数目,v数组标记每个节点属于哪个连通块。

C o d e : Code: Code:

void dfs(int x)
{
	v[x] = cnt;
	for (int i = head[x]; i; i = nxt[i])
	{
		int y = ver[i];//边的终点
		if (vis[y]) continue;
		dfs(y);//递归遍历
	}
}
for (int i = 1; i <= n; i++)//在main函数中 
{
	if (!v[i])
	{
		cnt++;
		dfs(i);
	}
} 

树与图的广度优先遍历

广度优先遍历需要使用队列实现。起初,队列只有一个起点元素(例如 1 1 1号节点).在广度优先遍历的过程中,我们从队头不断取出(删除)一个节点 x x x,对于 x x x面对的多条分支,把沿着每条分支到达的下一节点(当然是未访问过的节点)插入队尾重复执行上述过程知道队空为止即可。
下面的代码就是使用广度优先遍历 ( b f s ) (bfs) (bfs)历来遍历一张图(本代码使用STL,不懂的可以上网搜寻资料):

C o d e : Code: Code:

void bfs()
{
	memset(d, 0, sizeof(d));
	queue<int> Q;
	Q.push(1);d[1] = 1;
	while(!Q.empty())
	{
		int x = Q.front();
		Q.pop();
		for (int i = head[x]; i; i = nxt[i])
		{
			int y = ver[i];
			if (d[y]) continue;
			d[y] = d[x] + 1;
			Q.push(y);
		}
	}
}

从代码中可以看出,广度优先搜索的性质:
先访问完所有的第 i i i层节点后再开始访问第 i + 1 i+1 i+1
而且任意时刻,队列中至多有两个层次的节点。其中一部分属于第 i i i层,另一部分属于第 i + 1 i+1 i+1层。并且所有 i i i层节点排在所有第 i + 1 i+1 i+1层节点之前。
和深搜一样复杂度为 O ( N + M ) O(N+M) O(N+M)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值