BFS与DFS

一、BFS

1.BFS模板

(1)介绍

BFS 是 “广度优先搜索”(Breadth-First Search)的缩写,是一种用于图形和树结构遍历的算法。BFS 从一个起始节点开始,首先访问所有与该节点距离为1的节点,然后访问距离为2的节点,依此类推,直到访问所有节点或者找到目标节点。具体步骤如下

初始化

  • 创建一个队列,用于存储待访问的节点
  • 将起始节点放入队列中,并标记为已访问

遍历过程

  • 从队列的前端取出一个节点,称为当前节点
  • 访问当前节点的所有邻接节点(即与当前节点直接相连的节点)
  • 对于每个邻接节点,如果它未被访问过,将其放入队列,并标记为已访问

重复

  • 重复步骤2,直到队列为空或找到目标节点

广度优先搜索的特征为从起点开始,由近及远(树结构,只管的解释就是一层一层的搜索,队列中最多同时存在2层以内的元素节点)进行广泛的搜索。因此,目标顶点离起点越近,搜索结束得就越快


(2)BFS与DFS对比

BFS与DFS的区别:

bfs 遍历节点是先进先出(使用的工具为队列),dfs遍历节点是先进后出(使用栈)

bfs 是按层次访问的,dfs 是按照一个路径一直访问到底,当前节点没有未访问的邻居节点时,然后回溯到上一个节点,不断的尝试,直到访问到目标节点或所有节点都已访问

bfs 适用于求源点与目标节点距离最近的情况,例如:求最短路径。dfs 更适合于求解一个任意符合方案中的一个或者遍历所有情况,例如:全排列、拓扑排序、求到达某一点的任意一条路径


(3)BFS模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 节点数
const int N = num;
// 表示图的连接情况的邻接表
vector<Node> adj[N+1];
// 记忆节点是否已经访问
vector<int> visited[N+1];
vector<Node> q;

int BFS(Node start, Node target)
{
入队(初始状态);
visited[初始状态] = true;
while(!空队)
{
// 队首元素获取
Node front = 出队()
// 遍历所有与front节点邻接的节点
for(Node node: adj[front])
{
// 若改节点已经访问过则跳过
if (visited[node] == true)
continue;
// 该节点就是目标,则停止,返回
if (node == target)
{
输出答案;
return 0;
}
// 对当前节点打标记
v[node] = true;
}
}
return -1;
}

如下图,对其进行 BFS 遍历,若先遍历左子树,则顺序是FBGADICEH,由此知,对于树,BFS的遍历方式就是层序遍历

image-20240729100651742


(4)八数码问题

视频教程跳转

题目

1
2
3
4
3×3的棋盘上摆有八个棋子,每个棋子上标有18的某一数字。棋盘中留有一个空格,空格用0来表示。空格周围上下左右相邻的棋子可以移到空格中。
现给出原始状态和目标状态,求实现从初始布局到目标布局的最少步骤(初始状态的步数为0

答案:5

img

样例输入输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
【输出格式】
输出移动所用最少的步数。
【样例1输入】
2 8 3
1 6 4
7 0 5
1 2 3
8 0 4
7 6 5
【样例1输出】
5

【样例2输入】
2 8 3
1 6 4
7 0 5
0 1 2
3 4 5
8 7 6
【样例2输出】
17

如下图可以将该图抽象为BFS问题如下,每个节点有四种移动方式,就是0可以上下左右进行移动,当然需要判断移动是否合法

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#include <bits/stdc++.h>
using namespace std;
typedef struct node{
int m[3][3],parent = -1;
// m是状态矩阵,parent是父节点
}M;

int equal(M a, M b){
// 比较两个矩阵是否相等,相等返回1,不相等返回0
for(int i = 0; i < 3; i++)
for(int j = 0; j < 3; j++)
if(a.m[i][j] != b.m[i][j])
return 0;
return 1;
}

void printM(M a){
// 打印M中的矩阵
for(int i = 0; i < 3; i++){
for(int j = 0; j < 3; j++)
cout << a.m[i][j];
cout << endl;
}
cout << endl;
}

// 使用动态数组充当OPEN表、CLOSE表
vector <M> OPEN,CLOSE;
// cnt为OPEN的指针,控制搜索的进度
int ifFindAnswer = 0,cnt;
int opt[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
//操作集 上 下 左 右
int main() {
M *s0, *sg, front;// s0:初始状态,sg:最终状态,front:每次OPEN中取出队首元素
int x, y; // 用来记录0(即空格)的位置
s0 = (struct node *) malloc(sizeof(struct node));
sg = (struct node *) malloc(sizeof(struct node));
cout << "请输入初始状态:(请用0代替空格)" << endl;
// 读入初始状态和最终状态
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
cin >> s0->m[i][j];

cout << "请输入目标状态:(请用0代替空格)" << endl;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
cin >> sg->m[i][j];

// 将s[0]放入队列第一个
OPEN.push_back(*s0);
while (OPEN.size() != cnt)
{
// 取出队首元素
front = OPEN[cnt];
// 判断是否找到答案 ,如果是,退出循环
if (equal(front, *sg) == 1)
{
ifFindAnswer = 1;
break;
}
// 1.查找0(也就是空格)位置并记录
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (front.m[i][j] == 0)
{
x = i; // 行
y = j; // 列
break;
}
}
}
// 遍历 上下左右四种方向移动
for (int i = 0; i < 4; i++)
{
int flag = 1;
// 判断当前移动是否合法
if (x + opt[i][0] > -1 && x + opt[i][0] < 3 && y + opt[i][1] > -1 && y + opt[i][1] < 3)
{
// 临时遍历,存储当前节点的上一个节点情况
M temp = front;
swap(temp.m[x][y], temp.m[x + opt[i][0]][y + opt[i][1]]);
// 判断是否已探索过该节点
for (int j = 0; j < CLOSE.size(); j++)
{
if (equal(CLOSE[j], temp) == 1)
{
flag = 0;
break;
}
}
// 判断是否是已加入OPEN,但未探索的结点
for (int i = x + 1; i < OPEN.size(); i++)
{
if (equal(OPEN[i], temp) == 1)
{
flag = 0;
break;
}
}
if (flag == 1)
{
// flag==1,未探索过该节点,将结点加入OPEN
temp.parent = cnt;
OPEN.push_back(temp);
}
}
else
continue;
}
// 对当前节点标记已访问
CLOSE.push_back(OPEN[cnt++]);
}

if (ifFindAnswer == 0)
{
cout << "找不到路径!!";
}
else
{
// 找到答案,递归输出路径
cout << "找到路径,路径是:" << endl;
stack <M> ans;
ans.push(OPEN[cnt]);
while (ans.top().parent != 0)
{
ans.push(OPEN[ans.top().parent]);
}
ans.push(OPEN[0]);
while (ans.size() != 0)
{
printM(ans.top());
ans.pop();
}
}
}



二、BFS与DFS

视频教程跳转

一般搜索算法的流程框架

1.定义点集 X 为已经处理的点(去重),点集 F 为已发现但尚未处理的点(BFS队列,DFS栈

2.初始化点集 X 为空,点集 F 只包含搜索的起点

3.只要点集 F 不为空,循环4-6

  • 4.从点集 F 中取出一点 v
  • 5.处理点v,将点 v 加入点集 X
  • 6.遍历 v 的出边,对于每个 v 的邻居,若既不在点集 X 中也不在点集 F 中,则加入点集 F

7.搜索结束,点集 X 里的点搜索过的点


1.递归DFS

递归DFS,不需要认为的创建栈,而是使用系统栈

流程框架如下:

1.定义点集 X 为已经处理的点(去重

2.初始化点集 X 为空

3.递归搜索点 v (开始时搜索起点)

  • 4.处理点 v ,将点 v 加入点集 X
  • 5.遍历 v 的出边,对于每个 v 的邻居,若不在点集 X 中,则递归处理该点

6.搜索结束,点集X里的点都为搜索过的点

使用栈的DFS与递归DFS的区别

下图左边为使用栈的DFS,右图为使用递归的DFS

image-20240729141827833




三、BFS的基础理论

1.BFS的基础理论

视频教程跳转

特点

从根节点开始,一圈一圈,一层一层向外遍历

从近到远,由内而外

如何实现

使用队列

  • 源点入队
  • 源点出队,距离为1的子节点入队
  • 距离为1的点出队,距离源点距离为2的点入队(距离为1的节点的子节点)
(1)基本代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <bits/stdc++.h>
using namespace std;

// 节点数量
int N;
// 标记节点是否被访问,节点从1开始
bool v[N+1];
// 邻接表,是i节点相连接的下一个节点的集合
vector<int> adj[N+1];
// 队列
queue<int> q;

// BFS算法,s为根节点
void bfs(int s)
{
// 根节点入队
q.push(s);
// 标记访问
v[s] = true;

// 队列不为空
while (!q.empty())
{
// 队首元素出队
int cur = q.front();
q.pop();

// 遍历与cur邻接的子节点
for(auto next: adj[cur])
{
// 当前节点已经访问过,跳过
if(v[next]) continue;
// 没有访问过,则入队
q.push(next);
v[next] = true;
}
}
}

(2)棋盘图扩展方式(重点)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <bits/stdc++.h>
using namespace std;

// 节点数量
int N;
// 标记节点是否被访问,节点从1开始
bool v[N+1][N+1];
// 队列--存放坐标
queue<pair<int, int>> q;

void bfs(int sx, int sy)
{
// 源点入队
q.push({sx, sy});
// 源节点标记访问
v[sx][sy] = true;

// dx, dy 组合就是棋盘的上下左右运动
int dx[] = {0,0,1,-1};
int dy[] = {1,-1,0,0};

// 队列不为空
while(!q.empty())
{
// C++14 特性 对首元素出队
auto[curx, cury] = q.front();
q.pop();

// 遍历上下左右
for(int i = 0;i<4;i++)
{
int nx = curx + dx[i];
int ny = cury + dy[i];
// 当前移动是否合法
if(0<nx && nx<=N && 0<ny && ny <= N)
{
// 当前节点访问过,跳过
if(v[nx][ny])
continue;
q.push({ny,ny});
v[nx][ny] = true;
}
}
}
}

(3)BFS能做但DFS不能做

无权图的最短路径


(4)岛屿数量

问题描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例1
输入:grid1 = [
[“1”,“1”,“1”,“1”,“0”],
[“1”,“1”,“0”,“1”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“0”,“0”,“0”]
]
输出:1

---------------
示例2
输入:grid2 = [
[“1”,“1”,“0”,“0”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“1”,“0”,“0”],
[“0”,“0”,“0”,“1”,“1”]
]
输出:3

代码实现

核心思路:使用棋盘扩展方式的 BFS 将一个区域山下左右连续的1全部变为0,一个棋盘可以这样使用几次 BFS 就表示有几座岛屿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <bits/stdc++.h>
using namespace std;

class Solution
{
private:
queue<pair<int, int>> q;
int cnt= 0;
int n, m;

// 棋盘上的上下左右运动
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};

public:
void bfs(int i, int j, vector<vector<char>> &grid)
{
// 源点入队
q.push({i, j});
// 将该节点变为0
grid[i][j] = '0';

// 队列不为空
while(!q.empty())
{
// 队首元素出队
auto p = q.front();
q.pop();

// 遍历上下左右
for(int i=0;i<4;i++)
{
int nx = p.first + dx[i];
int ny = p.second + dy[i];

// 判断移动是否合法
if(0<=nx && nx<n && ny<=0 && ny<m)
{
// 判断当前位置是否为'1'
if(grid[nx][ny] == '1')
{
// 将其变为 '0'
grid[nx][ny] = '0';
q.push({nx, ny});
}
}
}
}
}
};


int numIslands(vector<vector<char>> &grid)
{
n = grid.size();
m = grid[0].size();

for(int i = 0;i<n;i++)
{
for(int j = 0;j<m;j++)
{
if(grad[i][j] == '1')
{
bfs(i, j, grid);
cnt++;
}
}
}
return cnt;
}