搜索与图论---DFS和BFS、树与图的存储和遍历

  • 深度优先搜索 DFS
  • 广度优先搜索 BFS
  • 树与图的存储
  • 树与图的深度优先遍历
  • 树与图的广度优先遍历
  • 拓扑排序

DFS与BFS

DFS

尽可能往深处搜,当搜到头的时候才会回溯,然后继续向深处搜索。
DFS首先要考虑的是以何种顺序把某一道题的所有可能方案全部搜一遍

可以看成是一个非常执着的人

两个DFS的重要概念:回溯和剪枝

  1. 回溯
    当走到头,无路可走的时候,先后

  2. 剪枝
    提前判断当前的方案一定是不合法的,不用遍历其之后的方案,直接剪掉。


现在看一个最经典的只涉及到回溯的题:AW842

搜索与图论---DFS和BFS、树与图的存储和遍历_第1张图片
虽然样式很像一棵树,但是每次存储的时候存储的都是一条路径,当回溯的时候相对应的无用路径会被删除。而在写函数时就有一个隐含的栈来帮我们实现这个回溯的。这个栈结构可联想递归时的函数运行顺序。

回溯要注意的一点:回溯要恢复现场,即从碰壁回到原来可以分支的状态

dfs函数的结构如下:
#include
#include
using namespace std; 
const int N=10;

int n;
int path[N];   //状态数组,存储走的哪条路径
//而且再每次触底返回再重新搜索时之前的数组会被覆盖,所以只需要开一个数组就好了
bool state[N]; //一个bool数组来记录这个数是否被用过(true是用过了)

void dfs(int u)  //传入的参数代表的是当前处理第几层
{
	if(u==n)  //总共都只有n层,当u当前处理层数碰到底了,则输出并回溯
	{
		for(int i=0;i<n;i++) printf("%d ",path[i]);
		cout<<endl;
		return;
	}
	for(int i=1;i<=n;i++)  //u
	{
		if(!state[i])   //如果这个数没有被用进path数组里则加进path数组,然后标记它已被使用过
		{
			path[u]=i;
			state[i]=true;//进行操作
			dfs(u+1);     //向下一层搜
			//path[u]=0;这句可以不写 ,因为要被后面的步骤覆盖的 
			state[i]=false;  //恢复现场
		}
	}
}

int main()
{
	cin>>n;
	dfs(0);  //传入的参数代表的是当前处理第几层
	return 0;
}


再看一个经典的n皇后问题:AW843

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

一种对角线的截距是b=y-x由于可能出现负数,所以统一加上一个偏移量n使b>0即当col[i]时,udg[n+i-u] (i和看作纵坐标y)
搜索与图论---DFS和BFS、树与图的存储和遍历_第2张图片
同理
当col[i]时,dg[u+i] (i和看作纵坐标y)
搜索与图论---DFS和BFS、树与图的存储和遍历_第3张图片

//第一种搜索顺序
#include
#include
using namespace std; 
const int N=20;

int n;
char g[N][N];   //存储每一排的棋盘状态
//而且再每次触底返回再重新搜索时之前的数组会被覆盖,所以只需要开一个数组就好了
//bool state[N]; 这是上题的状态判断数组,在本题中,状态判断要用三个数组实现
bool col[N],dg[N],udg[N];  //列、对角线、反对角线

void dfs(int u)  //传入的参数代表的是当前处理第几层
{
	if(u==n)  //总共都只有n层,当u当前处理层数碰到底了,则输出并回溯
	{
		for(int i=0;i<n;i++) puts(g[i]);   //输出的是一排的棋盘状态
		cout<<endl;
		return;
	}
	
	for(int i=0;i<n;i++)  //u
	{
		if(!col[i]&&!dg[u+i]&&!udg[n-u+i])   //如果这个数没有被用进path数组里则加进path数组,然后标记它已被使用过
		{
			g[u][i]='Q';
			col[i]=dg[u+i]=udg[n-u+i]=true;   //进行操作
			dfs(u+1);     //向下一层搜                                                                                                                                       面的步骤覆盖的 
			col[i]=dg[u+i]=udg[n-u+i]=false;  //恢复现场
			g[u][i]='.';
		}
	}
}

int main()
{
	cin>>n;
	for(int i=0;i<n;i++)
		for(int j=0;j<n;j++)
			g[i][j] = '.';
		
	dfs(0);  //传入的参数代表的是当前处理第几层
	return 0;
}

BFS

一层一层地搜,这层遍历完了才会搜下面一层

可以看成是一个稳重的人,每次都要一层遍历完了才会再走一层,不会离家太远
(先吃窝边草再吃更远的草)

题目凡是涉及到最小步数、最短距离、问最少操作几次等都是BFS(前提是没有带权边)

BFS的基本框架
  1. 初始状态放到队列里面去 queue
  2. while(队列不空)
  3. while循环里:每一次把队头拿出来;扩展队头

例题1:走迷宫AW844
搜索与图论---DFS和BFS、树与图的存储和遍历_第4张图片
搜索与图论---DFS和BFS、树与图的存储和遍历_第5张图片

#include
#include
#include
#include
#include
using namespace std; 

typedef pair<int,int> PII;
const int N=110;

int n,m;
int g[N][N];	//g数组存的地图
int d[N][N];	//d数组存的每一个点到起点的距离
PII q[N*N];     //q数组存的坐标

int bfs()
{
	int hh=0,tt=0;   //队头队尾指针,模拟队列
	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};  
	//当前点向四个方向扩展的代码不需要写四个判断,可以用向量来表示
	//dx和dy数组的每一对只分别代表上(-1,0)右(0,1)下(1,0)左(0,-1)的向量
	
	while(hh<=tt)
	{
		PII t=q[hh++];  //出队列操作,对应“把队头拿出来”
		
		for(int i=0;i<4;i++)				//向四个方向判断
		{
			int x=t.first+dx[i],y=t.second+dy[i];   //x朝每个方向移动
			if(x>=0&&x<n&&y>=0&&y<m&&g[x][y]==0&&d[x][y]==-1)   
			//前四个判断:没有越地图的界||第五个判断:那个坐标可走(不是墙)||第六个判断:坐标没有被走过
			{
				d[x][y]=d[t.first][t.second] +1;    //BFS深度加一
				q[++tt]={x,y};  //队尾入队:把此刻的坐标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;
}

例题2:八数码 AW845

这题是求最短要多少步能达到正确排列,很显然是一题BFS题。

难点

  1. 状态表示比较复杂(这个状态表示是一个3x3的矩阵)

  2. 已知状态之后怎么转化到下一步

问题解决

  1. 在BFS里有两个量要进行表示:队列和距离
    队列直接定义queue来存
    距离直接定义unordered_map来存

  2. 第一步把这个字符串在脑子里转化为一个3x3的图
    第二步是枚举x可能移动的四个方向
    第三步是再次表示和存储处理后的图 搜索与图论---DFS和BFS、树与图的存储和遍历_第6张图片

#include 
#include 
#include 
#include 

using namespace std;

int bfs(string state)
{
    queue<string> q;		//存储状态队列
    unordered_map<string, int> d;		//存储距离(BFS层数)  (distance)

    q.push(state);
    d[state] = 0;

    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};	//向量表示:上右下左

    string end = "12345678x";   //正确排列
    while (q.size())	//队列不空
    {
        string t = q.front();
        q.pop();

        if (t == end) return d[t];	//处理结束,返回最短距

		//状态转移
        int distance = d[t];
        int k = t.find('x');		//string的find函数返回值为x的下标
        int x = k / 3, y = k % 3;	//将一个一维数组的下标转化成二维数组的下标(x,y)
        for (int i = 0; i < 4; i ++ )
        {
            int a = x + dx[i], b = y + dy[i];		//对各方向进行枚举
            if (a >= 0 && a < 3 && b >= 0 && b < 3)	//每个方向都没有出界
            {
                swap(t[a * 3 + b], t[k]);   //状态更新,是将字符串原来的x与枚举的点进行交换
											//a*3+b为二位坐标转化为一位数组
                if (!d.count(t))	//返回一个数在哈希表中的个数
                {
                    d[t] = distance + 1;	
                    q.push(t);
                }
                swap(t[a * 3 + b], t[k]);	//状态复原
            }
        }
    }

    return -1;
}

int main()
{
    char s[2];

    string state;
    for (int i = 0; i < 9; i ++ )
    {
        cin >> s;			//读入初始字符串
        state += *s;
    }

    cout << bfs(state) << endl;

    return 0;
}

DFS和BFS对比

实现它所用的数据结构 实现方法 占用空间 最短性
DFS stack 递归 O(h) 不具有最短性
BFS queue 非递归 O(2h) 能求出最短路

可以看出,DFS在空间复杂度上有着绝对优势,但BFS的优点是可以得到最短路径

最短路问题与动态规划问题的关系

最短路问题是包含dp问题的,即dp是一种特殊的(没有环的)最短路

你可能感兴趣的:(算法基础课学习记录)