DFS 深度优先搜索
基本概述
DFS (Depth First Search)
深度优先遍历 俗称暴搜
数据结构 stack 空间 O ( h ) O(h) O(h)
不具有最短性
DFS本质上就是递归,要想学会DFS就要理解好递归。
递归就是"自己"调用"自己"的过程,但是一定要设置递归的终止条件,调用函数的过程就是将函数进行压栈执行,如果不设置终止条件,函数就会不断的调用自己,也就是一直压栈下去,最终爆栈。
重要的点有三个:
- 递归终止条件如何设置
- 返回值应该是什么,该传递给上一层什么信息
- 这一层的递归中应该做什么工作
经典例题
排列数字
给定一个整数 n n n,将数字 1 ∼ n 1∼n 1∼n 排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。
输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
思路:
题目要求就是要求出 n n n个数字的全排列。
假设 n = 3 n=3 n=3时,那么作为开头的数字就有三个,所以要找出每一种数字开头都有哪些组合,如下图所示,是一个DFS的搜索过程。在搜索每个分支的时候都要进行判断,当前组合的数字是否没有重复?如果没有重复,就可以走到叶子结点的部分,每个叶子结点都是一种数字的排列组合。
有 n n n个数字,那么以每个数字开头的情况就有 n n n个,所以需要重复 n n n次搜索过程,搜索出每一个数字作为开头情况时的排列组合。搜索的过程就像是一棵树,不但要考虑好树的分支个数,还要考虑树的深度,每个叶子结点都是排列出来的结果,这个结果中包含了 n n n个数字,那它的上一层就是 n − 1 n-1 n−1个数字,以此类推一直到达第0层也就是根结点的位置,一共有n + 1个数字,所以需要控制好搜索的深度,从第0层开始,搜索到第 n n n层。
在排列数字的过程中需要判断数字是否重复出现,如果没有重复出现就可以将该数字纳入到组合中去,否则就判除该数字。为了判断数字是否重复出现我们可以开辟一个数组,用于标记每一个数字是否重复出现,但是要注意,DFS是一个递归的过程,所以在每一层递归都要记得”恢复现场“。
代码:
import java.util.*;
public class Main {
static final int N = 20;
static int[] p = new int[N]; // 用于存放排列好的数字
static boolean[] st = new boolean[N]; // 判断数字是否出现过的数组
static int n;
public static void dfs(int u) {
// 当搜索层数等于树的深度时,就可以输出结果
if (u == n) {
for (int i = 0; i < n; i++) {
System.out.print(p[i] + " ");
}
System.out.println();
return; // 要注意回溯
}
// 以每个数字开头的情况有n中,根节点上就有n个分支,所以需要循环n次,每次都搜索到最深处找到答案
for (int i = 1; i <= n; i ++) {
if (!st[i]) {
p[u] = i; // 将第n层的不重复数字放到数组中
st[i] = true; // 将这个数字标记为出现过
dfs(u + 1);
st[i] = false; // 恢复现场,将此数字标记为未出现过
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
dfs(0); // 从第0层,也就是根节点进行搜索
}
}
n-皇后问题
n n n−皇后问题是指将 n n n 个皇后放在 n × n n×n n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
输入样例:
4
输出样例:
.Q..
...Q
Q...
..Q.
..Q.
Q...
...Q
.Q..
思路:
与排列数字的题有相似之处,皇后棋子是否放在棋盘格子上的,棋盘的格子 n ∗ n n * n n∗n,那么从第一行开始,依次查找每个格子,判断是否可以将棋子放到格子上;
也就是说需要进行n次DFS,搜索每一行的棋子位置,同时与排列数字的题一样,需要考虑树的深度的问题;一共是n列,那么树深就是n + 1(因为树根从0开始,一直到第n行就是树的最深处)。皇后棋子不能存在于同一行同一列还有同一个对角线,因此需要设置三个数组用于标记是否有棋子出现在对角线的位置上。
还要注意用于记录对角线的数组的下标换算问题,可以将我们的这个棋盘看成是一个平面直角坐标系,有 x x x轴和 y y y轴。因此对角线就是形如 y = x + b y = x + b y=x+b或 y = − x + b y = -x + b y=−x+b的直线。直线方程中的 b b b就是我们要计算出的下标;因此需要设置两个数组来记录两种方向的对角线上的棋子出现。
代码:
import java.util.*;
public class Main {
static final int N = 20; // 将数组长度设置为20防止下标越界问题
static int n;
static char[][] g = new char[N][N];
static boolean[] col = new boolean[N], dg = new boolean[N], udg = new boolean[N];
public static void dfs(int row) {
if (row == n) { // 判断是否每一行都填上了棋子(是否到了树的最大深度)
for (int i = 0 ;i < n; i++) {
for (int j = 0; j < n ; j++)
System.out.print(g[i][j]);
System.out.println();
}
System.out.println();
return; // 回溯
}
// 用有n列,就需要dfs处理n次
for (int i = 0; i < n; i++) {
if (!col[i] && !dg[n - row + i] && !udg[row + i]) { // 判断列上,两个对角线上是否出现过
g[row][i] = 'Q'; // 填入棋子
col[i] = dg[n - row + i] = udg[row + i] = true; // 标记为已经填入棋子
dfs(row + 1); // 递归处理下一行
col[i] = dg[n - row + i] = udg[row + i] = false; // 结束递归后恢复现场
g[row][i] = '.'; // 同上
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
// 在dfs前需要初始化棋盘
for (int i = 0 ;i < n; i++)
for (int j = 0; j < n ; j++)
g[i][j] = '.';
// 也可以使用Arrays.fill()方法来初始化
// for (char[] c : g) {
// Arrays.fill(c, '.');
// }
dfs(0); // 从0开始的dfs
}
}