BFS

算法模板:BFS和DFS问题

Posted by SFHJavaer on 2022-07-21
Estimated Reading Time 10 Minutes
Words 2.7k In Total
Viewed Times

BFS和DFS问题

广度优先遍历是利用队列实现的搜索算法,一层一层的进行递归,典型例题是之字形遍历树树的层序遍历以及前中后序遍历

BFS的关键是队列,而DFS的关键是递归(也就是隐式的栈),回溯可以理解成是DFS的一种应用,只要是不撞南墙不回头都是DFS。

应用上:
1
2
3
BFS 常用于找单一的最短路线,它的特点是 "搜到就是最优解"
而 DFS 用于找所有解的问题,它的空间效率高,而且找到的不一定是最优解,必须记录并完成整个搜索,当然回溯不符合条件就会提前剪枝,这也是和普通的递归区别所在,需要满足边界条件以及判断条件两个,才能继续向下递归,不然直接返回上一步状态(回溯了),故一般情况下,深搜需要非常高效的剪枝(剪枝的概念请百度)。
DFS保证了避免“走老路”,返回标记位置的时候要将原场景恢复到第一次到达该节点的情况。

DFS的三种情况:

先上模板,当然不一定和模板一模一样,大致思路如此,比如标记着一步可能是隐式(比如指数型就两种情况,直接手动枚举情况了)的或者不需要,但是像结束条件是一定会有的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void dfs(int step)
{
判断边界//判断是否符合条件
{
相应操作
}
枚举每一种可能
{
标记 //防止多次重复枚举同一个方案
继续下一步dfs(step+1)
恢复初始状态(回溯的时候要用到)//恢复现场
//这就是DFS的精髓之处
//为的就是不影响下一种方案的枚举
}
}

第一种:指数型枚举

从 1∼n 这 n 个整数中随机选取任意多个,输出所有可能的选择方案

分析: 从1~n依次考虑 选 和 不选 两种情况。 一共 n 个数,每个数有 2 种情况。总共方案数为 2的n次方。所以被称为指数型枚举。
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
const int N = 1e1 + 6; 定义一个常量N
int n;
int st[N]; 记录每个位置当前的状态:0表示还没考虑,1表示选它,2表示不选它

void dfs(int u) 枚举的第几个数字
{
if(u > n) { 终止条件,因为题目要求一个就n个数 所以只有 u>n 就输出枚举的方案
//这个算法的精髓就是遍历到u>n也就是到底了,撞了南墙就输出一个,回溯恢复现场然后让原标记点继续向下遍历,刚开始的第一下肯定是到底的
for(int i = 1; i <= n; i++)
//1代表选了
if(st[i] == 1)
printf("%d ", i);
puts("");
return;
}
//情况只有两个枚举
st[u] = 1; 选它的分支
dfs(u + 1);
st[u] = 0; 恢复现场,以便进行下一个分支


st[u] = 2; 不选它的分支
dfs(u + 1);
st[u] = 0; 恢复现场
}
int main(void)
{
scanf("%d", &n);
dfs(1);
return 0;
}

排列型枚举,下面用全排列举例

把 1∼n 这 n 个整数排成一行后随机打乱顺序,输出所有可能的次序。

分析:依次枚举 1~n数 放到哪个位置,排列问题需要考虑顺序,需要book数组来判重。

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
const int N = 10;
int st[N]; 存储方案
bool book[N]; 标记数字是否被用过,true表示被用过,false表示没被用过
定义在里全局变量,所以现在book数组都是false 表示都没被用过
int n;
void dfs(int u){ 当前枚举第u位
//每凑齐一个排列就输出
if(u > n){
for(int i = 1; i <= n; i ++)
printf("%d ", st[i]); 打印方案
puts("");
return ;
}
for(int i = 1; i <= n; i ++){ 依次枚举每个数
if(!book[i]) 如果当前数没被用过
{ //下面两行是递归前标记
st[u] = i; 在第u位是i
book[i] = true; 标记用过

dfs(u + 1); 递归到下一位

//下面两行恢复现场,以便回溯其他情况
st[u] = 0; 可省略 因为写不写都会被 st[u]=i; 这一层给覆盖掉
book[i] = false; 不可省
}
}
}
int main(){
scanf("%d", &n);
dfs(1);
return 0;
}

组合型枚举

从 1∼n 这 n 个整数中随机选出 m 个,按字典序输出所有可能的选择方案。

组合问题和排列问题不同,不需要考虑顺序,即 1234 和 4321 都是一种组合.
因为要按字典序输出,所以需要从上一次枚举的数开始来依次枚举,控制局部枚举的数,使得新枚举的数比之前的大。
组合问题不需要考虑顺序,需要x来记录上一次的枚举的数。这样也就可以保证同一种组合只有一种顺序被打印。
因为是从上一次枚举的数开始枚举所以不会保存选用重复数字, 所以不需要定义去重数组book。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int n,m;
int cnt[30]; 记录方案
void dfs(int u,int x){ u //记录枚举了几位也就是边界 x记录了枚举了到了几
if(u>m) { 边界
for(int i=1;i<=n;i++)
cout<<cnt[i]<<" ";
puts(""); 换行
return;
}
for(int i=x;i<=n;i++) 从x开始枚举
{
cnt[u]=i;
dfs(u+1,i+1);//长度+1是肯定的,为什么是i+1呢?
//原因是长度加1的时候,当然x作为当前数也一定要后移了,不然i不变岂不是被枚举的值一直不变,导致组合一直无法向后推进,长度增加值往后搜索,因为是求”组合“随机挑数,每一次(某一位置的枚举)的挑选方式都有O(n2)种,所以必须是for+dfs而不是 简单for
cnt[u]=0; 恢复现场
}
}
int main()
{
cin>>n>>m;
dfs(1,1);
return 0;
}

BFS扯扯淡

BFS可以解决最短(路径)问题,原因是BFS是层序遍历,第一个满足条件的目标节点一定是最短最近的那一个节点。

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
71
72
73
74
75
76
package bfs;

import java.util.*;
//固定终点,改变矩阵看到达重点distance,如果修改为指定步数是否能达到,其实就是最后比较下大小,因为bfs解决了最短路径
public class Main {
public static void main(String[] args) {
int[][] tab = {
{0, 1, 0, 0, 0},
{0, 1, 0, 1, 0},
{0, 0, 0, 1, 0},
{0, 1, 1, 1, 0},
{0, 0, 0, 0, 0}
};
bfs(tab);
}

public static void bfs(int[][] tab) {
//记录走到该坐标需要的最短步数
int[][] distance = new int[5][5];
//记录移动的四个方位,在i循环中满足规则,比如i=0,代表x向右一个单位,因为一次只能移动一格,所以此时y必须对应为0,所以x和y中必有一个0;
int dx[] = {1,0,-1,0}, dy[] = {0,1,0,-1};
Queue<Point> queue = new LinkedList<Point>();
queue.add(new Point(0 ,0));
while(!queue.isEmpty()) {
Point p = queue.poll();
//到达终点
if(p.getX() == 4 && p.getY() == 4)
break;
//for中无敌递归,原因是bfs是依赖于队列的
for(int i = 0; i < 4; i++) {
//产生下一步坐标
int x = p.getX() + dx[i];
int y = p.getY() + dy[i];
//越界判断,障碍判断,是否走过判断,必须得判断走过,如果BFS不进行标记的话,那就变成死循环了,会走以前的老路,但是为什么层序遍历不需要呢?
//原因是层序遍历head会被pop,接着操作的只有left和right,所以不可能产生网上回去的情况
if(x >= 0 && y >= 0 && x < 5 && y < 5
&& tab[x][y] != 1 && distance[x][y] == 0) {
queue.add(new Point(x, y));
//第一次经过的节点值一定是最小的,原因是上一步已经确定是最短的了,所以走这一步+1肯定也是最短的,就算queue中下一个节点也能扩展到这个节点
//但是它扩展也是需要1的步数,和第一次经过的节点步数是完全一样的,所以完全可以直接避免回头而一块判重,distance[x][y]保存第一次就好
//第一次经过一定是最短的,想想层序遍历,这里的每一个for只是一个节点的扩展,相当于一个node取left和right,层是没有明确划分的,一直判断满足不为空即可
// 每一层的节点距离一定是相等的,看谁先达到末尾。
//从一个角慢慢一层一层扩展到右下角,
distance[x][y] = distance[p.getX()][p.getY()] + 1;
}
}
}
System.out.println(distance[4][4]);
}

}

//点类,用于存储坐标(x,y),不写成class的话,不能将其加入queue中
//写成map不行吗?map其实也行,只要能获取x,y即可,判重是通过distance是不是0判断的。
class Point {
int x;
int y;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public Point(int x, int y) {
super();
this.x = x;
this.y = y;
}
}

BFS模板(其他的容器以及变量需自己定义)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void BFS ()
{
① 初始化队列
② 将起点入队
③ 标记起点已经被访问
while( 队列不空时 )
{
⑤ 取出队首元素 u ,存放到 t 变量里 ,u 元素出队
⑥ 扩展 t 结点
{
⑦ 判重新节点
⑧ 如果未被访问,且满足要求,记录当前点到起点的距离
⑨ 标记扩展的新结点
⑩ 将扩展的新节点入队
}
}

}

TOP K问题

两种思路:快排或者堆排

后者主要用在求最大最小的一个或多个(连续的)值,快排适用于求第K个最大最小以及获取多个最大最小的元素,快排求多个元素的话(比如获取最大的K个元素):在获取第k大的基础上,把index保存一下,不直接return了,而是完成整个快排,然后最后遍历原array从array.length-K到arr.length-1


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !