深搜dfs+广搜bfs+树与图的遍历+拓扑序列(模板篇acwing)

293 阅读13分钟

目录

深度优先搜索dfs:

例题:排列数字

例题:n-皇后问题

广度优先搜索bfs:

例题:走迷宫

树和图的遍历:

树和图的存储:

有向图:

 邻接表:

树和图的遍历:

例题:树的重心

例题:图中点的层次

拓扑序列

 例题:有向图的拓扑序列


深度优先搜索dfs:

实现数据结构:stack栈

特点:不撞南墙不回头

空间 :O(h)(h表示树的高度)

不具备最短路

例题:排列数字

问题:

给定一个整数n,将数字1~n排成一排,将会有很多种排列方法,请你按照字典序将所有的排列方法输出

输入样例:3

输出样例:
0 1 2
0 2 1
1 0 2
1 2 0
2 0 1
2 1 0

模拟过程:

注意模拟不撞南墙不回头这个思想:

我们从第0层开始向下搜索:即dfs(0),一开始的st[N]数组都是为0的(全局变量不初始化,默认为0),那么我们的第0层就按照for循环的顺序填上path[0]=0,并将0标记为1,表示已经用过了,后面不能再使用了,接下来,我们搜索下一层:即dfs(u+1),此时要注意并不是马上就执行st[i]=0这个操作,而是重新开始执行函数,此时参数为u+1,同样的,还是进入for循环,此时因为已经标记了0,所以0不满足条件,下一个则是1,所以在第一层填上1,至此path已经填好了path[0]=0 path[1]=1 ,接下来重复操作则有path[2]=2

path[2]=2的下一步时dfs(u+1),注意此时u==n,可以执行第一个if语句了,将我们的path数组n大小之前的数输出【0 1 2】并且return回溯

回溯:返回到调用这次递归前的那一层,也就是我们填入path[2]=2的那一层,即第二层:也就是说那一层我们已经执行完了,dfs(u+1)这个操作,按照顺序我们要执行st[i]=0,此时的i很明显是2,相当于我们用了2,在搜索下一层时,我们先标记2不能用,在它搜索回来时,我们将2取消标记

再次for循环:按照for循环的顺序,由于2已经用了,虽然它回来时解除了标记,但是我们已经用不到了,因为i++,导致现在i的值是3,然后我们将3填入path,即此时为path[2]=3,意味着我们的第二层又增加了一个组合:【0,1,3】,相当于碰到底后返回,并且向另外一个分支走去,这就是dfs!

#include<iostream>
using namespace std;

const int N=10;
int n;
int path[N];//存放答案
bool st[N];//标记该元素是否被使用过

void dfs(int u)
{
    if(u==n)//当遍历到最后一层时,输出该层
    {
        for(int i=0;i<n;i++) printf("%d ",path[i]);
        printf("\n");
    	return;//回溯
    }
    for(int i=0;i<n;i++)
    {
        if(!st[i])//如果该元素没有被使用过,那么就使用该元素
        {
            path[u]=i;//存储
            st[i]=1;//true标记为已经用过了
            dfs(u+1);//向下一层进行搜索
            st[i]=0;//回复现场
        }
    }
}
int main()
{
	cin>>n;
    dfs(0);
}

例题:n-皇后问题

问题:

n-皇后问题是指将 n 个皇后放在 n∗n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。

现在给定整数n,请你输出所有的满足条件的棋子摆法。

 输入样例:4

输出样例:

.Q..
...Q
Q...
..Q.

..Q.
Q...
...Q
.Q..

思路解析:

因为每行每列每一斜线都只能放置一个皇后,那么我么就需要三个数组来判断这个位置是否能放置数组:即列数组,正对角线数组,反对角线数组

问题:为什么不是用四个数组来判断?为什么没有行数组?

答:像上题一样,我们是一层层的判断,每一层的一种情况,我们只放置一个数,那么就确保了:每一行只会有一个元素,所以不需要再开一个行数组进行判断

模拟过程:

整体与上面的排列数字一样,多加了一个mp[u][i]='.'的操作,也就是说我回溯回来的时候,证明了这个点的放置是与其他点有冲突的,所以需要重新将已排好的点再次赋值成原始的模样,有一种天然的对称关系^ ^

#include<iostream>
using namespace std;
const int N = 20;
int n;
char mp[N][N];
bool col[N], dg[N], udg[N];
void dfs(int u) 
{
	if (u == n) 
	{
		for (int i = 0; i < n; i++) puts(mp[i]);
		puts("");
		return;
	}
	for (int i = 0; i < n; i++) {
		if (!col[i] && !dg[u + i] && !udg[n - u + i])
		{
			mp[u][i] = 'Q';
			col[i] = dg[u + i] = udg[n - u + i] = 1;
			/*u+i  n-u+i可以分别看作x+y=b,y-x=b的截距
				一条对角线对应一个截距,可以将这个截距映射到数组下标中
			*/
			dfs(u + 1);
			col[i] = dg[u + i] = udg[n - u + i] = 0;
			mp[u][i] = '.';
		}
	}
}
int main() {
	cin >> n;
	for (int i = 0; i < n; i++) 
    {
		for (int j = 0; j < n; j++) 
        {
			mp[i][j] = '.';

		}
	}
	dfs(0);
	return 0;
}

广度优先搜索bfs:

实现数据结构:queue队列

特点:一层一层地走完

空间 :O(2^h)(h表示树的高度)因为每一层最坏的情况是满二叉树

具备最短路

模板:

初始状态入队
while queue不空:
​	t<----队头
​	扩展队头

例题:走迷宫

问题:

给定一个n*m的二维整数数组,用来表示一个迷宫,数组中只包含0或1,其中0表示可以走的路,1表示不可通过的墙壁。

最初,有一个人位于左上角(1, 1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。

请问,该人从左上角移动至右下角(n, m)处,至少需要移动多少次。

数据保证(1, 1)处和(n, m)处的数字为0,且一定至少存在一条通路

模拟过程:

首先需要两个数组:一个是地图数组(用来初始化) g[N][N]:又称邻接矩阵

一个是距离数组(用来记录这个点到起点地距离)     d[N][N]

此外我们需要用队列来记录方位:此时套用模板

(实际上任何数据结构都可)但习惯队列

迭代条件:当队列不为空时(即队尾大于队头)

迭代过程:一开始是将起点位置放入队列中,然后遍历起点的四个方位,并用x,y分别记录可能到达的方位,如果满足条件:(在迷宫内且该位置未走过(d数组的值为-1))那么就将该位置入队,并在距离数组中将其标记+1,意味着从上一个点到这个点的距离为1,也就是距离起点的位置为1,此后再以这个点的四个方位进行扩展

返回:距离数组中的终点坐标

#include<iostream>
using namespace std;

typedef pair<int, int> PII;
const int N = 110;
int n, m;
int g[N][N];//地图
int d[N][N];//距离  
PII q[N * N];//模拟队列

int bfs() 
{
	int hh = 0, tt = 0;//hh指向队头元素,tt指向队尾元素 
	q[0] = { 0,0 };
	memset(d, -1, sizeof(d));//没有经历到这个点时,初始化均为-1
	d[0][0] = 0;//开始元素距离起点地距离为0
	int dx[4] = { -1,0,1,0 }, 
    int dy[4] = { 0,1,0,-1 };//移动方位
	while (hh <= tt) 
    {
		auto t = q[hh++];
		for (int i = 0; i < 4; i++) 
		{
			int x = t.first + dx[i], y = t.second + dy[i];
			if (x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1) 
			{
				/*注意d[x][y]==-1代表该点没有更新过距离
					也就是没有走过
				*/
				d[x][y] = d[t.first][t.second] + 1;
				q[++tt] = { x,y };
			}
		}
	}
	return d[n - 1][m - 1];
}
int main() 
{
	cin >> n >> m;
	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
			cin >> g[i][j];

	cout << bfs() << endl;
	return 0;
}

升级版:路径追踪

如果想要知道从起点到终点的最短路径经过的坐标,那我们还需要一个数组path来记录经过的坐标

代码实现:

#include<iostream>
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m;
int g[N][N];//地图
int d[N][N];//距离  
PII q[N * N];//模拟队列
PII Path[N][N];
int bfs() 
{
	int hh = 0, tt = 0;//hh指向队头元素,tt指向队尾元素 
	q[0] = { 0,0 };
	memset(d, -1, sizeof(d));
	d[0][0] = 0;
	int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };
	while (hh <= tt) {
		auto t = q[hh++];
		for (int i = 0; i < 4; i++) {
			int x = t.first + dx[i], y = t.second + dy[i];
			if (x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1) 
			{
				/*注意d[x][y]==-1代表该点没有更新过距离
					也就是没有走过
				*/
				d[x][y] = d[t.first][t.second] + 1;
				//下面是重点!!!
				Path[x][y] = t;
				q[++tt] = { x,y };
			}
		}
	}
	//求路径的过程
	int x = n - 1, y = m - 1;
	while (x || y) 
	{//只要x,y不同时为0,就继续向前转移
		cout << x << " " << y << endl;
		auto t = Path[x][y];
		x = t.first, y = t.second;
	}
	return d[n - 1][m - 1];
}
int main() 
{
	cin >> n >> m;
	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
			cin >> g[i][j];

	cout << bfs() << endl;
	return 0;
}

实质:

我们需要用Path数组来记录上一个被放入队列中的元素,那么我们需要对它开为pair,因为要存储坐标,但是如果只是存储坐标的话,会有大量没有用的坐标也存入Path数组里,所以我们需要从后往前找,即我是由哪个推出来的,那么我再变成“哪个推出来的”从而推出“哪个推出来的”


树和图的遍历:

树和图的存储:

有向图:

(1)邻接矩阵~~~~~二维数组

(2)邻接表~~~~~~单链表

邻接矩适合稠密图:稠密图:边的数量是点的数量的2~3倍

邻接表适合稀疏图:稀疏图:边的数量小于等于点的数量

邻接表:

#include<iostream>
using namespace std;
const int N=100010,M=N*2;
int h[N],e[M],ne[M],idx;
//h为每一个结点,e为该条边的权值,ne代表该条边指向的结点的下一条边
/*插入一条由a指向b的边*/
void add(int a,int b)
{
    //头插法
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

树和图的遍历:

可以想象成树的节点存储(下一个节点的地址和节点本身所存储的值)

或者单链表的的存储结构

#include<iostream>
using namespace std;
const int N=100010,M=N*2;
int h[N],e[M],ne[M],idx;
bool st[N];
/*
进入dfs函数后,先标记u点已经走过了,然后遍历u的邻接点
*/
void dfs(int u)
{
    st[u]=1;
    for(int i=h[u];i!=-1;i=ne[i])//头节点初始化为-1
    {
        int j=e[i];
        if(!st[j]) dfs(j);
    }
}


例题:树的重心

给定一颗树,树中包含n个结点(编号1~n)和n-1条无向边。

请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。

重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 1e5 + 10; //数据范围是10的5次方
const int M = 2 * N; //以有向图的格式存储无向图,所以每个节点至多对应2n-2条边

int h[N]; //邻接表存储树,有n个节点,所以需要n个队列头节点
int e[M]; //存储元素
int ne[M]; //存储列表的next值
int idx; //单链表指针
int n; //题目所给的输入,n个节点
int ans = N; //表示重心的所有的子树中,最大的子树的结点数目

bool st[N]; //记录节点是否被访问过,访问过则标记为true

//a所对应的单链表中插入b  a作为根 
void add(int a, int b) 
{
    e[idx] = b;//值存储
    ne[idx] = h[a];//头插:下一个地址指向头
    h[a] = idx++;//头++
}

//返回以u为根的子树中节点的个数,包括u节点
int dfs(int u) 
{
    int res = 0; //存储 删掉某个节点之后,最大的连通子图节点数
    st[u] = true; //标记访问过u节点
    int sum = 1; //存储 以u为根的树 的节点数, 包括u,如图中的4号节点

    //访问u的每个子节点
    for (int i = h[u]; i != -1; i = ne[i])//树的遍历
    {
        int j = e[i];
        //因为每个节点的编号都是不一样的,所以 用编号为下标 来标记是否被访问过
        if (!st[j]) //如果没有访问过
        {//dfs:向下寻找节点数
            int s = dfs(j);  // u节点的单棵子树节点数 如图中的size值
            res = max(res, s); // 记录最大联通子图的节点数
            sum += s; //以j为根的树 的节点数
        }
    }

    //n-sum 如图中的n-size值,不包括根节点4;
    res = max(res, n - sum); // 选择u节点为重心,最大的 连通子图节点数
    ans = min(res, ans); //遍历过的假设重心中,最小的最大联通子图的 节点数
    return sum;
}

int main() {
    memset(h, -1, sizeof h); //初始化h数组 -1表示尾节点
    cin >> n; //表示树的结点数

    // 题目接下来会输入,n-1行数据,
    // 树中是不存在环的,对于有n个节点的树,必定是n-1条边
    for (int i = 0; i < n - 1; i++) 
    {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a); //无向图
    }

    dfs(1); //可以任意选定一个节点开始 u<=n

    cout << ans << endl;

    return 0;
}

例题:图中点的层次

给定一个n个点m条边的有向图,图中可能存在重边和自环。

所有边的长度都是1,点的编号为1~n。

请你求出1号点到n号点的最短距离,如果从1号点无法走到n号点,输出-1

模拟过程:类似于bfs,用新的元素入队,去寻找更新的元素,只不过这里的方位存储在链表中,需要另一种遍历方式而已

#include<iostream>
using namespace std;
const int N = 1e5 + 5, M = N * 2;
int n, m;
int h[N], e[M], ne[M], idx;
int dis[N];
void add(int a, int b)//单链表头插法
{
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}
int bfs() 
{
	memset(dis, -1, sizeof(dis));//初始化距离为-1
	int q[N], hh = 0, tt = 0;//队头,队尾
	q[hh] = 1;//队头元素标记为1
	dis[1] = 0;//1本身到自身的距离为0
	while (hh <= tt) //当队列不为空
	{
		int u = q[hh++];//取下队头
		for (int i = h[u]; i != -1; i = ne[i])//图的遍历 
		{
			int v = e[i];//取下节点数
			if (dis[v] == -1) //节点数为-1,证明未走过
			{
				dis[v] = dis[u] + 1;//从u->v的距离为1:因为边长为1
				q[++tt] = v;//将新的元素入队->更新新的节点
			}
		}
	}
	return dis[n];//返回距离的终点即可
}
int main() 
{
	cin >> n >> m;
	memset(h, -1, sizeof(h));
	while (m--) 
	{
		int a, b;
		cin >> a >> b;
		add(a, b);
	}
	cout << bfs();
}

拓扑序列

有向无环图一定存在拓扑序列,也被称为拓扑图

若一个由图中所有点构成的序列A满足:对于图中的每条边(x, y),x在A中都出现在y之前,则称A是该图的一个拓扑序列。

一个有向无环图,一定至少存在一个入度为0的点

简单来说:就是a->b->c就是一个拓扑序列

而:a->b->c->a就不是一个拓扑序列

 例题:有向图的拓扑序列

给定一个n个点m条边的有向图,点的编号是1到n,图中可能存在重边和自环。

请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出-1。

若一个由图中所有点构成的序列A满足:对于图中的每条边(x, y),x在A中都出现在y之前,则称A是该图的一个拓扑序列

 代码实现:(待修改)

#include<iostream>
using namespace std;
const int N = 1e5 + 5;
int h[N], e[N], ne[N], idx;
int q[N], hh, tt = -1, n, m;
int deg[N];
void add(int a, int b)//头插
{
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}
bool bfs() 
{
	for (int i = 1; i <= n; i++) 
	{
		if (!deg[i]) 
		{
			q[++tt] = i;//将所有的点入队
		}
	}
	while (hh <= tt)//当队列不为空
	{
		int t = q[hh++];//取出队头元素
		for (int i = h[t]; i != -1; i = ne[i])//图的遍历
		{
			int j = e[i];//记录节点数
			deg[j]--;//
			if (deg[j] == 0) q[++tt] = j;
		}
	}
	return tt == n - 1;
}
int main() 
{
	cin >> n >> m;
	memset(h, -1, sizeof(h));
	while (m--) 
	{
		int x, y;
		cin >> x >> y;
		deg[y]++;
		add(x, y);
	}
	if (bfs()) 
	{
		for (int i = 0; i < n; i++) 
		{
			cout << q[i] << " ";
		}
	}
	else 
	{
		cout << -1;
	}
	return 0;
}