搜索 树与图的遍历 拓扑排序

文章详细介绍了深度优先搜索(DFS)和宽度优先搜索(BFS)在处理树与图问题中的应用,包括全排列、皇后问题的解决方案以及如何利用这两种方法进行拓扑排序和求解最短路径问题。此外,还提供了迷宫问题的BFS解法和有向无环图的拓扑序列计算。
摘要由CSDN通过智能技术生成


一 深度优先搜索dfs

特点:可以采用栈来实现,空间性能比较好,与高度呈正比。
全排列
第一种方法:采用搜索解决

#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
using namespace std;
#define endl '\n'
#define  ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 200003, null = 0x3f3f3f3f;
int n;
int path[N];//用来存储所有数字的状态
bool vis[N];//默认初始化为false,标记这个数字没有被使用过
void dfs(int u)
{
	if(u==n)//说明每个位置都已经填满,输出即可
	{
		for(int i=0;i<n;i++) cout<<path[i]<<' ';
		cout<<endl;
	}
	for(int i=1;i<=n;i++)
	{
		if(!vis[i])//如果这个数字没有被用过
		{
			path[u]=i;//将这个数字填入
			vis[i]=true;//标记这个数字已经被使用过
			dfs(u+1);//递归
			vis[i]=false;//恢复
		}
	}
}
int main()
{
	ios;
	cin>>n;
	dfs(0);//从第0个位置开始
	return 0;
}

第二种方法:采用函数解决next_permutation()
代码实例:

#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
using namespace std;
#define endl '\n'
#define MAXSIZE 1000
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 1e6 + 10, null = 0x3f3f3f3f;
int n;
int a[N];
int main()
{
	ios;
	cin>>n;
	for(int i=0;i<n;i++) 
	{
		a[i]=i+1;
		cout<<a[i];
	}
	cout<<endl;
	while(next_permutation(a,a+n))//判断当前数组中是否存在下一个排列,如果存在的话,会自动改变数组中的值,直接输出即可
	{
		for(int i=0;i<n;i++) cout<<a[i];
		cout<<endl;
	}
	return 0;
}

皇后问题
算法步骤
①逐行枚举,每行只有一个皇后
基本思想:由于每一行、每一列以及对角线上只能有一个皇后,因此,可以枚举每一行,依次遍历每一行当中的所有点,判断皇后应该放在什么地方,设置三个标记数组,标记这个点所在的列以及对角线上是否有皇后,如果这个点所在的这一列以及对角线上都没有皇后,则把皇后放在这个位置上,然后进行标记,标记完成之后,进行递归,递归完成之后,注意恢复。

#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
using namespace std;
#define endl '\n'
#define MAXSIZE 1000
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 1e6 + 10, null = 0x3f3f3f3f;
int n;
char str[100][100];//用来存储多个字符串
bool li[100];//标记某个点所在的那一列是否有皇后
bool di[100];//标记某个点所在的主对角线上是否有皇后
bool udi[100];//标记某个点所在的反对角线上是否有皇后
void dfs(int u)
{
	if(u==n)//说明所有皇后已经摆放完毕
	{
		for(int i=0;i<n;i++) cout<<str[i]<<endl;//每行输出一个字符串
		cout<<endl;
		return ;
	}
	for(int i=0;i<n;i++)
	{
		if(!li[i]&&!di[u+i]&&!udi[n-u+i])//如果(u,i)这个点所在的那一列并且这个点所在的主对角线上和反对角线上都没有皇后,则把皇后放在这个位置,因为每一行中最多只有一个皇后,因此可以枚举行数。
		{
			str[u][i]='Q';//将皇后放在这个位置上
			li[i]=di[u+i]=udi[n-u+i]=true;//标记这个点所在的这一列以及所在的两条对角线上都有皇后
			dfs(u+1);//递归之后回溯
			li[i]=di[u+i]=udi[n-u+i]=false;
			str[u][i]='.';
		}
	}
}
int main()
{
	ios;
	cin>>n;
	//初始化操作
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			str[i][j]='.';
		}
	}
	dfs(0);
	return 0;
}

②逐点枚举
思想:依次枚举每个点,每个点只有两种情况,要么放皇后,要么不放皇后,如果第一行枚举完之后,则从下一行的第一个点开始枚举,设置一个变量s表示皇后的个数,如果枚举完所有的行之后,如果此时皇后的个数已经确定完,则依次输出即可。如果枚举的当前这个点不放置皇后的话,直接递归到下一层,如果放置皇后的话,如果这个点所在的这一行 这一列 以及对角线上都没有皇后的话,则就把皇后放在这个位置上,同时标记一下这个点所在的行 列以及对角线上已经存在皇后,然后递归到下一层,递归完成之后进行恢复。

#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
using namespace std;
#define endl '\n'
#define MAXSIZE 1000
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 1e6 + 10, null = 0x3f3f3f3f;
int n;
bool row[100],line[100],dg[100],udg[100];//标记数组,标记每个点所在的行、列以及对角线上是否有皇后
char str[100][100];//存放多个字符串
void dfs(int x,int y,int s)//逐个点进行搜索
{
	if(y==n) //如果第一行搜索完成,则返回下一行的第一个元素
	{
		y=0;
		x++;
	}	
	if(x==n)//全部搜索完成
	{
		if(s==n)//如果皇后的个数等于n的话,说明皇后的位置已经确定完,直接输出即可
		{
			for(int i=0;i<n;i++) cout<<str[i]<<endl;
			cout<<endl;
		}
		return ;
	}
	dfs(x,y+1,s);//不放置皇后
	if(!row[x]&&!line[y]&&!dg[x+y]&&!udg[x-y+n])//如果这个点所在的行 列以及对角线上都没有皇后的话,则把皇后放在这个位置
	{
		str[x][y]='Q';//将皇后放在这个位置
		row[x]=line[y]=dg[x+y]=udg[x-y+n]=true;//标记该点所在的行 列以及对角线上存在皇后
		dfs(x,y+1,s+1);//递归进入下一个点,由于放置了皇后,所以皇后的个数加一,s+1
				row[x]=line[y]=dg[x+y]=udg[x-y+n]=false;//恢复
					str[x][y]='.';
	}
}
int main()
{
	ios;
	cin>>n;
	//初始化,将数组中的所有元素全置为点
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			str[i][j]='.';
		}
	}
	dfs(0,0,0);//从第一个位置开始进行搜索,初始时,皇后的个数为0
	return 0;
}

二 宽度优先搜索(BFS)

算法应用范围:可以用来求解最短路径问题
框架:创建队列,只要队列不空,取队头,扩展队头。

问题实例:走迷宫,0表示可走,1表示不可走,从左上角开始,每次可以向任意一个方向(上下左右)移动一个位置,求出从左上角移动到右下角至少需要移动多少次。

算法思想:用一个二维数组a来存储迷宫中的所有点,二维数组d来表示每个点到起始点的距离,队列q来存储每个点的坐标。首先初始化数组d中的所有元素为-1,代表该点没有被走过,起始点d[0][0]为0,代表被走过,用hh表示对头指针,tt表示队尾指针,队头q[0[为坐标(0,0),数组dx表示x方向向量,数组dy表示y方向向量,只要队列不空,每次取出队头元素坐标,枚举四个方向,每个方向求出移动之后点的坐标,如果该点在迷宫范围内并且该点可走并且该点没有被走过(第一次搜索到的点一定是最短路径),则求出该点到起始点的距离,并且把该点存入到队列中(即下一次从这个位置开始),最终返回最后一个点到起点的距离即可。
如果要求路径的话只需要定义一个数组pre来存放每个点的前一个点,然后逆序输出即可。
代码:

//PII pre[N][N];//存储每个点的前一个点
//pre[x][y]=ans;//存储点x y的上一个点
int x=n-1,y=m-1;//从最后一个点开始
while(x||y)//只要x和y不同时为0
{
    cout<<x<<' '<<y;
    auto ans=pre[x][y];//求出点x y的前一个点
    x=ans.first;
    y=ans.second;
}

输入:5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0

输出: 8

代码实例:

#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
using namespace std;
#define endl '\n'
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 1e3 + 10, null = 0x3f3f3f3f;
int n,m;
int a[N][N];//存储迷宫中的所有元素
int d[N][N];//存储迷宫中的每个点到起始点的距离
PII q[N*N];//队列,每个元素为一个点
//PII pre[N][N];//存储每个点的前一个点,求路径时用
int bfs()
{
	memset(d,-1,sizeof d);//初始化所有的点到起始点的距离为为-1,代表该点没有走过
	d[0][0]=0;//第一个点就是起始点,距离为0,该点已经被走过
	int hh=0,tt=0;//队头指针和队尾指针
	q[0]={0,0};//队列中第一个元素为起始点坐标
	int dx[4]={-1,0,1,0};//x方向
	int dy[4]={0,1,0,-1};//y方向
	while(hh<=tt)//只要队列不空
	{
		auto ans=q[hh++];//取出队头元素
		for(int i=0;i<4;i++)//枚举四个方向,求出移动后的点
		{
			int x=ans.first+dx[i];//移动后的点的x坐标
			int y=ans.second+dy[i];//移动后得点的y坐标
			if(x>=0&&x<n&&y>=0&&y<m&&a[x][y]==0&&d[x][y]==-1)//如果该点坐标在迷宫范围内,并且该点可走且该点没有被走过
			{
				d[x][y]=d[ans.first][ans.second]+1;//求出该点到起始点的距离
				q[++tt]={x,y};//将该点存储到队列中
                //pre[x][y]=ans;//存储点x y的前一个点
			}
		}
	}
	/*int x=n-1,y=m-1;
	while(x||y)
	{
		cout<<x<<' '<<y<<endl;
		auto ans=pre[x][y];
		x=ans.first;
		y=ans.second;
	}//输出路径时用*/
	return d[n-1][m-1];//返回最后一个点到起始点的距离即可
}
int main()
{
	ios;
	cin>>n>>m;
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<m;j++)
		{
			cin>>a[i][j];
		}
	}
	cout<<bfs()<<endl;
	return 0;
}

三 树与图的存储

常用方法:邻接表表示法(用一个数组来表示每个链表的头指针,初始值置为-1,表示为空)
注意:无论是深度优先遍历还是宽度优先遍历,每个点都只被访问一次,因此可以用一个标记数组来标记结点是否被访问过。
树与图的深度优先遍历

void dfs(int u)//树与图的深度优先遍历,每个点只被访问一次
{
	vis[u]=true;
	for(int i=h[u];i!=-1;i=ne[i])
	{
		int j=e[i];
		if(!vis[j]) dfs(j);
	}
}

代码实例:给定一个树,编号为1~n,含有n-1条无向边,求出去除重心之后所有连通块中点数的最小值。
算法思想
依次求出删掉每个点之后所有连通块中的点数的最大值,再求最小值即可,就是所求答案。可以设置一个函数,函数返回以结点u为根节点的子树中点的数。设置一个变量sum表示以结点u为根结点的子树中点的数目,初始化为1,因为结点u也属于结点u的子树,同时也便于之后求除结点u的子树之外的连通块的点的数目,同时设置一个变量res表示以结点u为根节点的子树中所有连通块中的点数的最大值,初始化为0,遍历结点u的所有邻接点,求出每个结点在图中的编号,如果这个点没有被访问过的话,就求一下以这个结点为根节点的子树中点的数目cnt,由于这个点是结点u的子树,因此sum+=cnt,同时cnt与res求最大值,当结点u的所有邻接点遍历完成之后,此时res的值就是结点u的子树中所有连通块的点数的最大值,但是还要与n-sum求最大值,之后res的值才是去除结点u之后所有连通块中点数的最大值,然后再与ans求最小值即可。
输入:
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
输出:4

#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
using namespace std;
#define endl '\n'
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 1e3 + 10, null = 0x3f3f3f3f,M=2*N;
int n;
int ans=N;//表示去除重心之后,所有连通块中点的数目的最大值的最小值
int h[N],e[M],ne[M],idx;//数组h存储每个链表的头指针,e存储每个结点的值,ne存储每个结点的next指针,idx表示当前用到了哪个点
bool vis[N];//标记数组,无论是dfs还是bfs,每个点都只访问依次
void add(int a,int b)//在结点a所在的链表中插入b
{
	e[idx]=b;
	ne[idx]=h[a];
	h[a]=idx++;
}
int dfs(int u)//返回以结点u为根的子树中点的数目
{
	vis[u]=true;//标记数组,标记该点被访问过了
	int sum=1,res=0;//sum表示以节点u为根的子树中点的数目,res表示除了结点u之外,u的子树中所有连通块中点数的最大值
	for(int i=h[u];i!=-1;i=ne[i])//遍历结点u的所有邻边
	{
		int j=e[i];//求出结点i在树中的编号
		if(!vis[j])//如果树中该结点没有被访问过得话
		{
			int s=dfs(j);//用s表示以结点j为根的子树的数目
			res=max(res,s);//求出s与res的最大值
			sum+=s;//由于结点j属于结点u的子树,所有结点u的子树数目应加上s
		}
	}
	res=max(res,n-sum);//求出结点u的父结点所在连通块的点的数目,并且与res求最大值,最终res的值就是除了结点u之外,所有连通块中点的数目的最大值
	ans=min(ans,res);//求出所有最大值的最小值就是所求答案
	return sum;
}
int main()
{
	ios;
	cin>>n;
	memset(h,-1,sizeof h);//存放每个链表的头指针,初始化数组h中的元素为-1,表示为空
	for(int i=0;i<n-1;i++)
	{
		int a,b;
		cin>>a>>b;
		add(a,b),add(b,a);//由于是无向边,因此需要建立两条边
	}
	dfs(1);//从第几个位置开始搜索,可以为1,也可以为n,也可以为任意数字(但要小于等于结点总数)
	cout<<ans<<endl;
	return 0;
}

树与图的宽度优先遍历
思想:用邻接表存储树和图,具体做法为:用一个数组h来存储每个链表的头指针,初始化为-1,表示链表为空,用数组e来存储链表中每个结点的值,每个结点的值为树中结点的编号,数组ne来存储每个结点的next指针,用数组d来存储每个结点到根结点的距离,初始化所有元素为-1,表示该结点没有被访问过(第一次被访问的点一定是最短路径),初始化第一个结点到根节点的距离为0,表示该结点已经被访问过,用数组q来表示队列,初始化队列中第一个元素为树中第一个结点,只要队列不空,每次求出队头元素,队头元素指针++,遍历该元素在图中的所有邻边,每次求出链表中的所有结点在图中的编号,如果该结点没有被访问的话,就更新该结点到根节点的距离,并且将该结点入队。

宽度优先搜索框架
先把初始状态(1号结点)赋值给队头,只要队列不空,每次取出队头元素赋值给t,然后扩展t的所有邻接点x,如果x没有被访问过的话(因为只第一次访问才是最短路径),就更新一下x到根节点的距离,并且将x入队。

输入:

4 5
1 2
2 3
3 4
1 3
1 4

输出:1

#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
using namespace std;
#define endl '\n'
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 1e3 + 10, null = 0x3f3f3f3f,M=2*N;
int n,m;
int h[N],e[N],ne[N],idx;//数组h用来存储图中每个节点以及它的所有邻边组成的链表中的头结点,初始化为-1,表示链表为空
int d[N];//数组d存储每个结点到根节点的距离,初始化为-1,表示该结点没有被访问过
int q[N];//数组q表示队列,用来存储每个结点
void add(int a,int b)//在a所在的链表中插入结点b
{
	e[idx]=b;//链表中每个结点的值是该结点在图中的标号
	ne[idx]=h[a];
	h[a]=idx++;
}
int bfs()
{
	memset(d,-1,sizeof d);
	d[1]=0;//表示第一个点已经被遍历过
	int hh=0,tt=0;
	q[0]=1;//首先将第一个结点入队
	while(hh<=tt)
	{
		int t=q[hh++];//取出队头
		for(int i=h[t];i!=-1;i=ne[i])//遍历结点t的所有邻接点
		{
			int j=e[i];//求出链表中每个节点在图中的编号
			if(d[j]==-1)//如果图中该结点没有被访问过的话,第一次被访问的点一定是最短路径
        	{
				d[j]=d[t]+1;//求出结点j到根结点的距离
				q[++tt]=j;//将节点j入队
			}
		}
	}
	return d[n];//返回最后一个结点点到图中根节点的距离
}
int main()
{
	ios;
	cin>>n>>m;
	memset(h,-1,sizeof h);
	for(int i=0;i<m;i++)
	{
		int a,b;
		cin>>a>>b;
		add(a,b);//在a所在的链表中插入结点b,即插入边a->b
	}
	cout<<bfs()<<endl;
	return 0;
}

四 拓扑序列

宽度优先搜索的应用:求拓扑序列
只有有向无环图才有拓扑序列(起点总是在终点前面)。一个有向无环图一定至少存在一个入度为0的点。拓扑序列中一定不存在环。
输入:
3 3
1 2
2 3
1 3
输出:1 2 3
算法思想
采用图的邻接表存储方式将图存储起来,用队列q来存放图中所有度为0的点,用数组d来存储每个结点的入度,只要对队列不空,每次取出队头元素赋值给t,遍历t的所有邻接点,每次求出链表中结点在图中的标号j,将j结点的入度减一,如果j的入度变为0的话,就把结点j入队。最后判断所有结点是否都已经入队,如果队尾指针等于图中结点的个数减一,则说明所有结点都已经入队,则存在拓扑序列,否则不存在拓扑序列,此时依次输出队列中的元素就是一个拓扑序列。

代码实例:

#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
#include<map>
#include<unordered_set>
#include<unordered_map>
using namespace std;
#define endl '\n'
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 1e5 + 10, null = 0x3f3f3f3f,M=2*N;
const double eps = 1e-6;
const int P = 131;
int n,m;
int h[N],ne[N],e[N],idx;
int d[N];//存储每个结点的入度
int q[N];//存储拓扑序列
void add(int a,int b)//在a所在的链表中加入b
{
	e[idx]=b;
	ne[idx]=h[a];
	h[a]=idx++;
}
int bfs()
{
	int hh=0,tt=-1;
	for(int i=1;i<=n;i++)//遍历一下所有结点,将所有入度为0的结点全部进行入队
	{
		if(!d[i]) q[++tt]=i;
	}
	while(hh<=tt)//只要队列不空
	{
		int t=q[hh++];//每次取出队头元素的值
		for(int i=h[t];i!=-1;i=ne[i])//遍历一下t的所有邻边
		{
			int j=e[i];//求出每个结点在图中的编号
			d[j]--;//将每个结点的入度减去1
			if(!d[j])//如果减去1之后,这个节点的入度变为0的话,说明这个结点的入度为1,将其入队即可
			{
				q[++tt]=j;
			}
		}
	}
	return tt==n-1;
}
int main()
{
	ios;
	cin>>n>>m;
	memset(h,-1,sizeof h);//初始化每个链表的头指针为-1,表示链表为空
	while(m--)
	{
		int a,b;
		cin>>a>>b;
		add(a,b);//在a所在的链表中加入b,即加入a->b这条边,同时结点b的入度加1
		d[b]++;
	}
	if(bfs())//如果最终所有结点都入队的话,说明之前每个结点的入度均为1,直接输出队列当中的前n个元素即可,就是一个拓扑序列
	{
		for(int i=0;i<n;i++) cout<<q[i]<<" ";
	}
	else cout<<"-1"<<endl;//否则的话,说明存在入度不为1的结点,不存在拓扑序列,输出-1
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值