回溯和DFS的区别
- DFS是一种算法结构,回溯是一种算法思想。
- 回溯就是通过不同的尝试来生成问题的解,有点类似于穷举,但是和穷举不同的是回溯会“剪枝”。剪枝的意思也就是说对已经知道错误的结果没必要再枚举接下来的答案了,比如一个有序数列1,2,3,4,5,我要找和为5的所有集合,从前往后搜索我选了1,然后2,然后选3的时候发现和已经大于预期,那么4,5肯定也不行,这就是一种对搜索过程的优化。
- 回溯搜索是深度优先搜索(DFS)的一种。对于某一个搜索树来说(搜索树是起记录路径和状态判断的作用),回溯和DFS,其主要的区别是,回溯法在求解过程中不保留完整的树结构,而深度优先搜索则记下完整的搜索树。
- 为了减少存储空间,在深度优先搜索中,用标志的方法记录访问过的状态,这种处理方法使得深度优先搜索法与回溯法没什么区别了。
回溯
回溯算法就是个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置做⼀些操作,算法框架如下:
框架
写 backtrack 函数时,需要维护⾛过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时, 将「路径」记⼊结果集。
78. 子集(中等)
class Solution {
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
LinkedList<Integer> path = new LinkedList<>();
traverse(path, 0, nums);
return res;
}
public void traverse(LinkedList<Integer> path, int start, int[] nums) {
res.add(new LinkedList<>(path));
for (int i = start; i < nums.length; i++) {
path.add(nums[i]);
traverse(path, i + 1, nums);
path.removeLast();
}
}
}
77. 组合(中等)
这就是典型的回溯算法,k 限制了树的高度,n 限制了树的宽度,直接套我们以前讲过的回溯算法模板框架就行了:
class Solution {
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
LinkedList<Integer> path = new LinkedList<>();
traverse(path, 1, n, k);
return res;
}
public void traverse(LinkedList<Integer> path, int start, int n, int k) {
if (path.size() == k) {
res.add(new LinkedList<>(path));
return;
}
for (int i = start; i <= n; i++) {
path.add(i);
traverse(path, i + 1, n, k);
path.removeLast();
}
}
}
排列问题每次通过 contains 方法来排除在 track 中已经选择过的数字;而组合问题通过传入一个 start 参数,来排除 start 索引之前的数字。
46. 排列(中等)
class Solution {
List<List<Integer>> res = new LinkedList<>();
/* 主函数,输⼊⼀组不重复的数字,返回它们的全排列 */
public List<List<Integer>> permute(int[] nums) {
// 记录「路径」
LinkedList<Integer> path = new LinkedList<>();
backtrack(nums, path);
return res;
}
// 路径:记录在 path 中
// 选择列表:nums 中不存在于 path 的那些元素
// 结束条件:nums 中的元素全都在 path 中出现
public void backtrack(int[] nums, LinkedList<Integer> path) {
// 触发结束条件
if (path.size() == nums.length) {
res.add(new LinkedList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// 排除不合法的选择
if (path.contains(nums[i])) {
continue;
}
// 做选择
path.add(nums[i]);
//进⼊下⼀层决策树
backtrack(nums, path);
//取消选择
path.removeLast();
}
}
}
总结
子集问题可以利用数学归纳思想,假设已知一个规模较小的问题的结果,思考如何推导出原问题的结果。也可以用回溯算法,要用 start 参数排除已选择的数字。
组合问题利用的是回溯思想,结果可以表示成树结构,我们只要套用回溯算法模板即可,关键点在于要用一个 start 排除已经选择过的数字。
排列问题是回溯思想,也可以表示成树结构套用算法模板,不同之处在于使用 contains 方法排除已经选择的数字,
DFS
框架
// ⼆叉树遍历框架
public void traverse(TreeNode root) {
traverse(root.left);
traverse(root.right);
}
// ⼆维矩阵遍历框架
public void dfs(int[][] grid, int i, int j, Boolean[][] visited) {
int m = grid.length, n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
// 超出索引边界
return;
}
if (visited[i][j] == true) {
// 已遍历过 (i, j)
return;
}
// 进⼊节点 (i, j)
visited[i][j] = true;
dfs(grid, i - 1, j, visited);// 上
dfs(grid, i + 1, j, visited); // 下
dfs(grid, i, j - 1, visited); // 左
dfs(grid, i, j + 1, visited); // 右
}
200. 岛屿数量(中等)(FloodFill算法)
class Solution {
// 主函数,计算岛屿数量
public int numIslands(char[][] grid) {
int res = 0;
// 遍历 grid
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length; j++) {
if (grid[i][j] == '1') {
res++; // 每发现⼀个岛屿,岛屿数量加⼀
}
dfs(grid, i, j); // 然后使⽤ DFS 将岛屿淹了
}
}
return res;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海⽔
private void dfs(char[][] grid, int i, int j) {
if (i < 0 || j < 0 || i >= grid.length || j >= grid[0].length) return; // 超出索引边界
if (grid[i][j] == '0') return; //已经是海⽔了
grid[i][j] = '0';//将 (i, j) 变成海⽔
//淹没上下左右的陆地
dfs(grid, i - 1, j);
dfs(grid, i + 1, j);
dfs(grid, i, j - 1);
dfs(grid, i, j + 1);
}
}
1254. 统计封闭岛屿的数目(中等)
class Solution {
// 主函数,计算岛屿数量
public int closedIsland(int[][] grid) {
int n = grid.length, m = grid[0].length;
int res = 0;
// 遍历 grid
for (int j = 0; j < m; j++) {
dfs(grid, 0, j);
dfs(grid, n - 1, j);
}
for (int i = 0; i < n; i++) {
dfs(grid, i, 0);
dfs(grid, i, m - 1);
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 0) {
res++; // 每发现⼀个岛屿,岛屿数量加⼀
}
dfs(grid, i, j); // 然后使⽤ DFS 将岛屿淹了
}
}
return res;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海⽔
private void dfs(int[][] grid, int i, int j) {
if (i < 0 || j < 0 || i >= grid.length || j >= grid[0].length) return; // 超出索引边界
if (grid[i][j] == 1) return; //已经是海⽔了
grid[i][j] = 1;//将 (i, j) 变成海⽔
//淹没上下左右的陆地
dfs(grid, i - 1, j);
dfs(grid, i + 1, j);
dfs(grid, i, j - 1);
dfs(grid, i, j + 1);
}
}
1020. 飞地的数量(中等)
class Solution {
// 主函数,计算岛屿数量
public int numEnclaves(int[][] grid) {
int n = grid.length, m = grid[0].length;
int res = 0;
// 遍历 grid
for (int j = 0; j < m; j++) {
dfs(grid, 0, j);
dfs(grid, n - 1, j);
}
for (int i = 0; i < n; i++) {
dfs(grid, i, 0);
dfs(grid, i, m - 1);
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
res++; // 每发现⼀个岛屿,岛屿数量加⼀
}
}
}
return res;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海⽔
private void dfs(int[][] grid, int i, int j) {
if (i < 0 || j < 0 || i >= grid.length || j >= grid[0].length) return; // 超出索引边界
if (grid[i][j] == 0) return; //已经是海⽔了
grid[i][j] = 0;//将 (i, j) 变成海⽔
//淹没上下左右的陆地
dfs(grid, i - 1, j);
dfs(grid, i + 1, j);
dfs(grid, i, j - 1);
dfs(grid, i, j + 1);
}
}
1905. 统计子岛屿(中等)
public int countSubIslands(int[][] grid1, int[][] grid2) {
int n = grid1.length, m = grid1[0].length;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid2[i][j] == 1 && grid1[i][j] == 0) {
dfs(grid2, i, j);
}
}
}
int ret = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid2[i][j] == 1) {
ret++;
//这个岛屿肯定不是⼦岛,淹掉
dfs(grid2, i, j);
}
}
}
return ret;
}
//从 (i, j) 开始,将与之相邻的陆地都变成海⽔
private void dfs(int[][] grid2, int i, int j) {
int n = grid2.length, m = grid2[0].length;
if (i < 0 || j < 0 || i >= n || j >= m) return;
if (grid2[i][j] == 0) return;
grid2[i][j] = 0;
dfs(grid2, i - 1, j);
dfs(grid2, i + 1, j);
dfs(grid2, i, j - 1);
dfs(grid2, i, j + 1);
}
79. 单词搜索(中等)
阿里面试题,在二维数组中搜索“alibaba”,当时不会做。。
用模板写出来,虽然增加了很多无关的操作,但是想通过这道题举一反三。
后期可能会优化一下。
class Solution {
List<LinkedList<Character>> res = new LinkedList<>();//存放结果
public boolean exist(char[][] board, String word) {
boolean[][] visited = new boolean[board.length][board[0].length];//记录是否被访问过
LinkedList<Character> path = new LinkedList<>();//记录当前路径
//从不同起点出发
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
dfs(board, i, j, path, visited, word, 0);//dfs返回以i,j起点得到的满足题意的字符串
}
}
//res中存放多个满足题意的结果
if (res.size() == 0) return false;
return true;
}
private void dfs(char[][] board, int i, int j, LinkedList<Character> path, boolean[][] visited, String word, int k) {
int n = board.length, m = board[0].length; //row和col
if (i < 0 || j < 0 || i >= n || j >= m) return; // 超出索引边界
if (visited[i][j] == true) return; //访问过返回
visited[i][j] = true; // 已遍历过 (i, j)
path.add(board[i][j]); //将当前节点放到访问路径path中
if (path.get(k) != word.charAt(k)) {//节点不符合,提前剪枝
path.removeLast(); //撤销加入节点操作并将该节点设为false
visited[i][j] = false;
return;
}
if (k == word.length() - 1) { //满足题意返回,返回前撤销
res.add(new LinkedList<>(path));
path.removeLast();
visited[i][j] = false;
return;
}
dfs(board, i - 1, j, path, visited, word, k + 1);//上
dfs(board, i + 1, j, path, visited, word, k + 1);//下
dfs(board, i, j - 1, path, visited, word, k + 1);//左
dfs(board, i, j + 1, path, visited, word, k + 1);//右
visited[i][j] = false;
path.removeLast(); //撤销
}
}
BFS
待更