深度优先搜索(DFS)之习题分析
一、深度优先搜索(DFS)的概念
深度优先搜索(缩写DFS)有点类似广度优先搜索,也是对一个连通图进行遍历的算法。它的思想是从一个顶点V0开始,沿着一条路一直走到底,如果发现不能到达目标解,那就返回到上一个节点,然后从另一条路开始走到底,这种尽量往深处走的概念即是深度优先的概念。
深度优先搜索属于图算法的一种,是一个针对图和树的遍历算法,英文缩写为DFS即Depth First Search。深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相关的图论问题,如最大路径问题等等。一般用堆数据结构来辅助实现DFS算法。其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。
二、全排列
(一)、题目需求
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
(二)、解法
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<List<Integer>>();
if (nums == null) {
return null;
}
if (nums.length == 0) {
result.add(new ArrayList<Integer>());
return result;
}
ArrayList<Integer> list = new ArrayList<>();
dfs(result, nums, list);
return result;
}
private void dfs(List<List<Integer>> result, int[] nums, ArrayList<Integer> list) {
if (list.size() == nums.length) {
result.add(new ArrayList<>(list));
return;
}
for (int i = 0; i < nums.length; i++) {
if (list.contains(nums[i])) {
continue;
}
list.add(nums[i]);
dfs(result, nums, list);
list.remove(list.size() - 1);
}
}
(三)、代码分析
1、定义并初始化result、list集合。
ArrayList<Integer> list = new ArrayList<>();
2、list集合用来判断当前排列情况,若排列元素等于num的长度,则result增加该排列。并返回。
if (list.size() == nums.length) {
result.add(new ArrayList<>(list));
return;
}
3、遍历num数组,进行排列
(1)、若list中包含当前位置的元素,则说明该元素已被排列,查看下一元素是是否被排列。
(2)、若list中不包含当前位置的元素,则将其加入list中,表示该元素被排列。
(3)、递归dfs方法。直至返回退出
(4)、删去list中的最后一位元素,进行回溯操作
for (int i = 0; i < nums.length; i++) {
if (list.contains(nums[i])) {
continue;
}
list.add(nums[i]);
dfs(result, nums, list);
list.remove(list.size() - 1);
}
三、子集
(一)、题目需求
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
**说明:**解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
(二)、解法
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<List<Integer>>();
if (nums == null || nums.length == 0) {
List<Integer> list = new ArrayList<Integer>();
result.add(list);
return result;
}
List<Integer> list = new ArrayList<Integer>();
dfs(result, nums, list, 0);
return result;
}
private void dfs(List<List<Integer>> result, int[] nums, List<Integer> list, int start) {
result.add(new ArrayList<>(list));
for (int i = start; i < nums.length; i++) {
list.add(nums[i]);
dfs(result, nums, list, i + 1);
list.remove(list.size() - 1);
}
}
(三)、代码分析
1、该题于全排列思路一致,区别点在于递归和回溯的判断。定义并初始化result、list集合。
List<Integer> list = new ArrayList<Integer>();
2、list中存储着当前遍历结果产生的子集,首次增加list表示空集,result增加当前list
result.add(new ArrayList<>(list));
3、遍历num数组,进行子集查找。以每个位置的元素为开头元素,逐个查找其后的元素,将其后元素逐个加入list中,每加入一次则为一个子集。
(1)、将当前元素加入list中,则子集增加
(2)、递归进行dfs方法。result增加当前list表示的子集,并且for循环遍历开端位置右移一位,遍历下一个元素,将其加入list子集中。
(4)、删去list中的最后一位元素,进行回溯操作
for (int i = start; i < nums.length; i++) {
list.add(nums[i]);
dfs(result, nums, list, i + 1);
list.remove(list.size() - 1);
}
四、n皇后
(一)、题目需求
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
示例:
输入:4
输出:[
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。
提示:
- 皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。
(二)、解法
public List<List<String>> solveNQueens(int n) {
List<List<String>> result = new ArrayList<List<String>>();
if (n == 0) {
return result;
}
List<Integer> columns = new ArrayList<>();
search(n, result, columns);
return result;
}
private void search(int n, List<List<String>> result, List<Integer> columns) {
if (columns.size() == n) {
result.add(drawChessboard(columns));
}
for (int columnIndex = 0; columnIndex < n; columnIndex++) {
if (!isValid(columnIndex, columns)) {
continue;
}
columns.add(columnIndex);
search(n, result, columns);
columns.remove(columns.size() - 1);
}
}
private List<String> drawChessboard(List<Integer> columns) {
List<String> chessboard = new ArrayList<>();
for (int row = 0; row < columns.size(); row++) {
StringBuilder sb = new StringBuilder();
for (int column = 0; column < columns.size(); column++) {
sb.append(columns.get(row) == column ? 'Q' : '.');
}
chessboard.add(sb.toString());
}
return chessboard;
}
private boolean isValid(int columnIndex, List<Integer> columns) {
int row = columns.size();
for (int rowIndex = 0; rowIndex < columns.size(); rowIndex++) {
if (columns.get(rowIndex) == columnIndex) {
return false;
}
if (rowIndex - columnIndex == row - columns.get(rowIndex)) {
return false;
}
if (rowIndex + columnIndex == row + columns.get(rowIndex)) {
return false;
}
}
return true;
}
(三)、代码分析
1、首先分析题目需求:n皇后要求每一个皇后的位置不在同一行,不在同一列,不在同一斜线(斜线包括两条)一条斜线为左上角至右下角;一条斜线为右上角至左下角。
2、定义并初始化辅助集合columns,并进行search方法进行皇后位置摆放位置的查找
- columns的意义:
- (1)、columns的每个位置表示n皇后棋盘的每一行。例如下标为0的位置表示第一行;下标为“1”的位置表示第二行。
- (2)、columns的每个位置的元素表示n皇后棋盘在该位置表示的行里n皇后摆放的列位置。例如下标为0的位置的元素为3,则表示第一行里n皇后摆放的位置为第4列;下标为1的位置的元素为6,则表示第二行里n皇后摆放的位置为第7列。
- (3)、由于columns的每个位置表示每一行的n皇后的摆放位置,所以使得每个n皇后不会位于同一行。
List<Integer> columns = new ArrayList<>();
search(n, result, columns);
3、进行返回条件判断,若columns集合中的元素数量等于n,则说明n个皇后的位置皆找到了合适的位置进行摆放,则将其绘制成棋盘的形状加入result集合中,并返回。
if (columns.size() == n) {
result.add(drawChessboard(columns));
}
4、n皇后棋盘共有n列,columns已经确保了每一行不会出现2个及以上的皇后。现在通过遍历每一列,确定columns中的每个位置的元素,即确定每一行中n皇后应该位于哪一列,可使得它们不位于同一列,同一斜线。
递归进行search方法进行列位置的查找
并在错误情况下通过回溯返回
for (int columnIndex = 0; columnIndex < n; columnIndex++) {
if (!isValid(columnIndex, columns)) {
continue;
}
columns.add(columnIndex);
search(n, result, columns);
columns.remove(columns.size() - 1);
}
5、通过字符串的拼接,进行棋盘的绘制
private List<String> drawChessboard(List<Integer> columns) {
List<String> chessboard = new ArrayList<>();
for (int row = 0; row < columns.size(); row++) {
StringBuilder sb = new StringBuilder();
for (int column = 0; column < columns.size(); column++) {
sb.append(columns.get(row) == column ? 'Q' : '.');
}
chessboard.add(sb.toString());
}
return chessboard;
}
6、判断当前打算摆放的列位置是否会产生冲突
(1)、row:表示目前已经确定未发生冲突的行数;并且由于行是从0开始计算,所以row也表示目前要确定列位置的的行位置。例如:若columns.size() == 3,则表示前3行皆已找到不发生冲突的列位置;并且row == columns.size(),所以目前要确定第4行,也就是下标为“3”的行的列位置。
(2)、for循环需要将当前摆放的列位置与先前已经确定的每一行里的皇后位置进行判断,判断他们是否会在同一列,或者在同一行。因为columns.size()表示已经确定的行数,所以以columns.size()作为结束标志。
(3)、第一个if判断,判断当前要摆放的列位置,是否会与目前循环中的判断行里面的皇后位置产生位于同一列的冲突。例如columnsIndex == 2;当row == 3;rowIndex == 1时,即判断第4行第二列若摆放一个皇后是否会与第2行的皇后位置位于同一列。
(4)、第二个if判断,判断当前要摆放的列位置,是否会与目前循环中判断行里面的皇后位置产生位于同一斜线(左上角至右下角)的冲突。通过观察棋盘,可发现每一条左上角至右下角的斜线,其行下标减去列下标的差相等。
(5)、第三个if判断,判断当前要摆放的列位置,是否会与目前循环中判断行里面的皇后位置产生位于同一斜线(右上角至左下角)的冲突。通过观察棋盘,可发现每一条右上角至左下角的斜线,其行下标加上列下标的和相等。
private boolean isValid(int columnIndex, List<Integer> columns) {
int row = columns.size();
for (int rowIndex = 0; rowIndex < columns.size(); rowIndex++) {
if (columns.get(rowIndex) == columnIndex) {
return false;
}
if (rowIndex - columnIndex == row - columns.get(rowIndex)) {
return false;
}
if (rowIndex + columnIndex == row + columns.get(rowIndex)) {
return false;
}
}
return true;
}