算法基础课—搜索与图论(一)DFS、BFS
深度优先搜索 DFS
深度优先搜索DFS
一直往深的搜,直到碰到叶节点就回溯到上一节点进行搜索
核心——回溯和剪枝
DFS ——递归实现——回溯和剪枝
回溯
回溯——找到多个解
因为要回溯所以在算法中有很重要的一步——记录现场、恢复现场
回溯——递归实现
剪枝
一般用在对于有限制条件和寻找最优情况的判断
剪枝——边走边判断是否符合条件或者是否是最优解,如果不符合,可以直接不考虑这条分支
算法思想
1、循环子节点
2、结束条件判断
比方说如果u==n,则输出。。。
3、条件判断——子节点情况
(1)基础条件——没有访问过
(2)外部条件——题目所给的限制条件——剪枝
4、记录状态
(1)表示当前位置的
我现在是遍历到哪一个,下一个要遍历哪一个——一般作为形参来传递,遍历下一个一般+1
比方说最经常用的 u,u表示序列中第几个,遍历下一个u就加一
也有对于坐标的记录,记录x,y,表示当前点的位置,如果遍历下一个x,y更新
(2)表示是否访问过的
表示visit的数组,visit[i] 即第i个节点是否访问过
(3)恢复现场
5、 恢复现场——恢复子节点的状态——在递归回溯之后,一般在递归语句后面
(1)改成没有访问过——visit数组改成false
(2)在递归之前对其进行的操作的恢复
整体结构
整体结构
void dfs(....表示节点位置....)
for(。。多个子节点。。){
if(是否满足结束条件){
。。。。。
return;
}
if(条件判断){
visit[i] = true;
......执行相应操作,比方说状态更改或者是访问子节点更改子节点状态等等
dfs(....位置更新,更新到下一位置....)
visit[i] = false;
.....恢复状态.....
}
}
算法模板——来自于排列数的题目
void dfs(int u){
int i;
for(i = 1; i <= n; i ++){
if(u == n){
for(i = 0; i < n; i ++ ){//这里要从0开始因为u一开始从0开始存的path
cout<<path[i]<<" ";
}
cout<<endl;
return;
}
if(!st[i]){//如果没有用过
path[u] = i;
st[i] = true;
dfs(u + 1);
st[i] = false;
}
}
}
排列数字
题目
给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。
输入格式
共一行,包含一个整数 n。
输出格式
按字典序输出所有排列方案,每个方案占一行。
数据范围
1≤n≤7
输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
算法思想
算法思想:
dfs要知道下一个遍历到哪里,也要知道当前遍历的情况
于是用path存储当前遍历的情况,用u来表示遍历到哪一个位置,st来表示是否用过区分每一个节点
循环for(i=1;i<=n;i++)其实是对各种情况的分发,先在一个情况下进行遍历,如果说遍历到n时,则输出返回,回溯的时候要恢复父节点的状态
然后再继续到另一种情况下
深度优先
记录状态,遍历下一状态,遍历终止,回溯,恢复状态,继续遍历
遍历下一状态要明白怎么遍历到下一状态,如这里,就是用u来表示,我到第几个位置了。
记录状态,记录当前状态的情况,这里是st[]和path,同时要便于之后恢复状态,st,path
代码
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int a[N];
int n;
bool st[N];//用于判断是否用过
int path[N]; //记录数组
void dfs(int u){
int i;
for(i = 1; i <= n; i ++){
if(u == n){
for(i = 0; i < n; i ++ ){//这里要从0开始因为u一开始从0开始存的path
cout<<path[i]<<" ";
}
cout<<endl;
return;
}
if(!st[i]){//如果没有用过
path[u] = i;
st[i] = true;
dfs(u + 1);
st[i] = false;
}
}
}
int main(){
int i;
cin>>n;
dfs(0);
}
n皇后问题
题目
n− 皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
现在给定整数 n,请你输出所有的满足条件的棋子摆法。
输入格式
共一行,包含整数 n。
输出格式
每个解决方案占 n 行,每行输出一个长度为 n 的字符串,用来表示完整的棋盘状态。
其中 . 表示某一个位置的方格状态为空,Q 表示某一个位置的方格上摆着皇后。
每个方案输出完成后,输出一个空行。
注意:行末不能有多余空格。
输出方案的顺序任意,只要不重复且没有遗漏即可。
数据范围
1≤n≤9
输入样例:
4
输出样例:
.Q…
…Q
Q…
…Q.
…Q.
Q…
…Q
.Q…
方法一
1、如何表示当前位置
由于一行只可能有一个皇后,则可以将皇后位置转换为类似“1324”序列进行思考,其中数字则为每行皇后在的位置,则只需u来记录现在到第几行。
2、如何进行状态更新 u + 1 即到下一行,即到序列的下一个位置
3、条件判断,设置col[]数组,如果该行有,则col[i] =true;
正对角线 dg[]数组,横纵坐标相加的值,类似于y = -x + b,这个b值,如果对应dg[b] = true 则该对角线上存在数
反对角线 udg[]数组 y= x + b,b = y - x ,由于b小于0 ,所以加上一个n 所以dg[n + y - x] = true ,则。。。
x,y坐标移动——上下左右,设置dx,dy数组
x 即为u ,y即为循环里的子节点序号
4、当前位置什么时候到达下一状态
循环子节点的情况,如果情况满足条件,进入下一状态
代码
#include <iostream>
using namespace std;
const int N = 20;
int n;
char g[N][N];
bool col[N], dg[N], udg[N];
void dfs(int u)
{
if (u == n)
{
for (int i = 0; i < n; i ++ ) puts(g[i]);
puts("");
return;
}
for (int i = 0; i < n; i ++ )
if (!col[i] && !dg[u + i] && !udg[n - u + i])
{
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;
}
方法二
1、当前位置的表示——x,y,s s表示有几个皇后
2、更新——x,y+1
3、结束标志s==n
4、条件判断与方法一类似
代码
#include <iostream>
using namespace std;
const int N = 10;
int n;
bool row[N], col[N], dg[N * 2], udg[N * 2];
char g[N][N];
void dfs(int x, int y, int s)
{
if (s > n) return;
if (y == n) y = 0, x ++ ;
if (x == n)
{
if (s == n)
{
for (int i = 0; i < n; i ++ ) puts(g[i]);
puts("");
}
return;
}
g[x][y] = '.';
dfs(x, y + 1, s);
if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n])
{
row[x] = col[y] = dg[x + y] = udg[x - y + n] = true;
g[x][y] = 'Q';
dfs(x, y + 1, s + 1);
g[x][y] = '.';
row[x] = col[y] = dg[x + y] = udg[x - y + n] = false;
}
}
int main()
{
cin >> n;
dfs(0, 0, 0);
return 0;
}
宽度优先搜索 BFS
第一层搜完搜第二层,一层一层搜索
最短性
BFS 搜到的点具有最短性,也就是他搜到的第一点一定是最短路径的点,而DFS不具有这个性质。所以凡是权值为1的最短路径的都是用BFS来做,而比较没那么明显思路的用DFS来做
空间复杂度的对比

适用情况
BFS ——距离、层数的搜索——适用于许多最短的情况
BFS与最短路,只有权重都为1,才可以用BFS求最短路
如果是深度搜索,能保证搜索到终点,但是不一定能保证搜到的是最短的
层可以是
(1)树、图的层
(2)表示相同状态
(3)到某点的距离
核心
1、状态的表示
可以是位置,可以是数字,可以是字符串,用来表示当前节点内容
2、层数——可以是具体的层、距离、某种情况
3、记录层数——对应状态下的对应层数
可以用二维数组、一位数组、map等记录
BFS模板
将初始情况压入队列
while(队列非空){
取出队头元素
对取出的队头元素进行条件判断(没有访问过+题目给的条件)
满足条件的进行扩展
将扩展的元素压入堆栈
}
代码模板——走迷宫的宽度有限搜索
while (q.size())
{
auto t = q.front();
q.pop();
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] = d[t.first][t.second] + 1;
q.push({x, y});
}
}
}
走迷宫
题目
给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。
最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。
数据保证 (1,1) 处和 (n,m) 处的数字为 0,且一定至少存在一条通路。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含 m 个整数(0 或 1),表示完整的二维数组迷宫。
输出格式
输出一个整数,表示从左上角移动至右下角的最少移动次数。
数据范围
1≤n,m≤100
输入样例:
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
算法思想
算法思想:
1、表示状态
用x,y的位置表示状态——pair<int ,int>
2、更新状态——如何更新状态,进入到下一个
3、层——这里是距离,到起点的距离
4、记录层数——d[][]数组,对应xy下的d
5、条件判断——在对子节点扩展的时候,要判断是否访问过+题目所给的条件
6、结束状态判断——题目所给的
如果说没有从这个结束状态判断中出来,说明可能不存在题目说的可能性
宽度有限搜索——队列,先把第一层的进队列,再把第二层的进队列,第二层就是与上一层距离为1的位置,如果该位置没有被访问且符合范围且不存在障碍物,则插入队列最为第二层
然后将队头出栈,再依次进行扩展。
题目类似于要我们求一条最短路径,权值为1的最短路径问题就可以用BFS,相当于每次进行距离为1的往下搜索,看最快如何能找到终点
代码
#include <iostream>
using namespace std;
typedef pair<int,int> PII;
const int N = 1000;
int a[N][N],d[N][N];//用来记录所在层数
int hh = 0, tt = 0;
PII q[N*N];//注意得是n*n才够大
int main(){
int n, m;
int i,j;
cin>>n>>m;
for(i = 1; i <= n; i ++ ){
for(j = 1; j <= m; j ++){
cin>>a[i][j];
}
}
q[0] = {1,1};
d[1][1] = 1;
int dx[] = {-1, 1, 0, 0},dy[] = {0, 0, -1, 1};
while(hh <= tt){
PII t = q[hh ++ ];
for(i = 0; i < 4; i ++){
int x = t.first + dx[i];
int y = t.second +dy[i];
if(x >= 1 && x <= n && y >= 1 && y <= m && !a[x][y] && !d[x][y]){//注意是要没有被访问过,没有被访问过用d[][] = 0 表示,d[][]这个数组用来表示所在层数
q[++ tt ] = {x, y};
d[x][y] = d[t.first][t.second] + 1;
}
}
}
int result = d[n][m] - 1;
cout<<result<<endl;
}
八数码问题
题目
在一个 3×3 的网格中,1∼8 这 8 个数字和一个 x 恰好不重不漏地分布在这 3×3 的网格中。
例如:
1 2 3
x 4 6
7 5 8
在游戏过程中,可以把 x 与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 x
例如,示例中图形就可以通过让 x 先后与右、下、右三个方向的数字交换成功得到正确排列。
交换过程如下:
1 2 3 1 2 3 1 2 3 1 2 3
x 4 6 4 x 6 4 5 6 4 5 6
7 5 8 7 5 8 7 x 8 7 8 x
现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。
输入格式
输入占一行,将 3×3 的初始网格描绘出来。
例如,如果初始网格如下所示:
1 2 3
x 4 6
7 5 8
则输入为:1 2 3 x 4 6 7 5 8
输出格式
输出占一行,包含一个整数,表示最少交换次数。
如果不存在解决方案,则输出 −1。
输入样例:
2 3 4 1 5 x 7 6 8
输出样例
19
算法思想
1、状态的表示
string ——字符串
2、层数——x移动的距离
3、记录层数——unordered_map<string, int> d;
4、结束标志判断
end = “12345678x”
if(now_str == end)
算法思想:
和走迷宫不同的是,走迷宫的标志使用点来表示的,如果到达终点就结束了,但是这道题的思路应该用字符串来表示,如果字符串与结束字符串相同,则说明到达终点。
同时用unordered_map 来记录d即距离
思路仍然还是
一层一层的搜索,如果之前没有出现过,则对其d进行+1,同时将字符串push到队列中
由于求得是最短路,所以用宽度优先搜索比较合适
宽度优先搜索
1、队列,记录当前状态
2、距离d——记录当前状态的所在层数
3、结束标志
将初始状态push到队列中
while 队列非空
取出队头元素
扩展状态
判断是否出现过,是否符合条件,符合则push进队列
如果到达结束的地方,则跳出循环。
代码
#include <iostream>
#include <unordered_map>
#include <queue>
#include <string>
using namespace std;
int n = 3;
typedef pair<string,int> PSI;
queue<string> q;
unordered_map<string, int> d;
int main(){
int t = 0, i;
int result = -1;
string q0;
char s[2];
for(i = 0; i < 9; i ++){
cin>>s;
q0 += s;
}
d[q0] = 0;//注意unordered_map的赋值方式,有点类似于python的字典
q.push(q0);
int dx[] = {-1, 1, 0, 0},dy[] = {0, 0, -1, 1};
string end = "12345678x";
while(q.size()){
auto now_str = q.front();
q.pop();
int now_distance = d[now_str];
int k = now_str.find('x');
if(now_str == end) {
result = d[now_str];
break;
}
for(i = 0; i < 4; i ++){
int x = k / 3 + dx[i];
int y = k % 3 + dy[i];
if(x >= 0 && x < n && y >= 0 && y < n){
swap(now_str[k], now_str[x * 3 + y]);
if(!d.count(now_str)){
d[now_str] = now_distance + 1;
//q[++ tt] = {now_f,}
q.push(now_str);
}
swap(now_str[k], now_str[x * 3 + y]);//恢复现场,因为还要进行下一次循环。
}
}
}
cout<<result<<endl;
}