DFS/回溯算法
回溯的核心思想在于:
但是面对不同的题目:做什么选择,在哪做选择都是需要考虑的问题。
大致的框架
1 2 3 4 5 6 7 8 9 10
| 结果集 = [] def backtrace(路径, 选择列表): if 满足结束的条件: 结果集.add(路径) return for 选择 in 选择列表: 做出选择 backtrace(路径, 选择列表) 撤销选择
|
例子
【全排列】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| List<List<Integer>> res;
public List<List<Integer>> permute(int[] nums) { res = new ArrayList<>(); List<Integer> path = new ArrayList<>(); backtrace(nums, path); }
void backtrace(int[] nums, List<Integer> path){ if(path.size() == nums.length){ res.add(new ArrayList<>(path)); return; } for(int i = 0; i < nums.length; i++){ if(path.contains(nums[i])) continue; path.add(nums[i]); backtrace(nums, path); path.remove(new Integer(nums[i])); } }
|
【N皇后】
在 n\(\times\)n 的棋盘上放置 n
个皇后,使得 n 个皇后不能相互攻击
1 2 3
| public List<List<String>> solveNQueens(int n){ List<Integer> board = new ArrayList<>(); }
|
子集划分
给定一个数组nums
和一个正整数k
,返回其是否能被分成k
个子集,且每个子集的元素和相同
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 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
| class Solution { public boolean canPartitionKSubsets2(int[] nums, int k) { if(k > nums.length) return false; int sum = 0; for(int i : nums) sum += i; if(sum % k != 0) return false;
int sub_sum = sum / k; int[] buckets = new int[k]; Arrays.sort(nums); int i = 0, j = nums.length - 1; for(; i < j; i++, j--){ int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; }
return backtrace(nums, 0, sub_sum, buckets); }
private boolean backtrace(int[] nums, int index, int target, int[] buckets){ if(index == nums.length){ for(int i = 0; i < buckets.length; i++){ if(buckets[i] != target) return false; } return true; }
for(int i = 0; i < buckets.length; i++){ if(buckets[i] + nums[index] > target) continue; buckets[i] += nums[index]; if( backtrace(nums, index + 1, target, buckets) ){ return true; } buckets[i] -= nums[index]; }
return false; }
public boolean canPartitionKSubsets1(int[] nums, int k) { if(k > nums.length) return false; int sum = 0; for(int i : nums) sum += i; if(sum % k != 0) return false;
int sub_sum = sum / k; int[] buckets = new int[k];
visited = new boolean[nums.length];
return backtrace(nums, 0, 0, buckets, sub_sum); }
boolean[] visited; private boolean backtrace(int[] nums, int start, int index, int[] buckets, int target){ if(index == buckets.length){ return true; }
if(buckets[index] == target) { return backtrace(nums, 0, index + 1, buckets, target); }
for(int i = start; i < nums.length; i++){ if(visited[i] || buckets[index] + nums[i] > target) continue; buckets[index] += nums[i]; visited[i] = true; if( backtrace(nums, i + 1, index, buckets, target) ){ return true; } visited[i] = false; buckets[index] -= nums[i]; }
return false;
}
int[] nums; int per, n; boolean[] dp;
public boolean canPartitionKSubsets(int[] nums, int k) { this.nums = nums; int all = Arrays.stream(nums).sum(); if (all % k != 0) { return false; } per = all / k; Arrays.sort(nums); n = nums.length; if (nums[n - 1] > per) { return false; }
dp = new boolean[1 << n]; Arrays.fill(dp, true); return dfs((1 << n) - 1, 0); }
public boolean dfs(int s, int p) { if (s == 0) { return true; } if (!dp[s]) { return dp[s]; } dp[s] = false; for (int i = 0; i < n; i++) { if (nums[i] + p > per) { break; } if (((s >> i) & 1) != 0) { if (dfs(s ^ (1 << i), (p + nums[i]) % per)) { return true; } } } return false; } }
|
回溯很重要的一个点:不能没有考虑后面情况就定死了前面的选择。回溯的本质在于先不断地做决定,如果成功那么之前的选择就是正确的并返回,如果出错了,在不断地撤回决定并且重新做决定
总结
总结回溯算法在子集、组合和排列三类问题上的处理
对于子集、组合和排列这三类问题,可以按照元素是是否可重、是否可复选两方面来划分。
子集问题和排列问题本质其实是同属一类问题,都是对顺序没有要求的选取部分序列...所以就将他们归类为一种问题
1、集合/组合问题
- 集合/组合问题是不存在顺序的,所以每次
backtrace
的过程中,不能都从下标0开始选择元素,所以每次进入backtrace
时都应该指明下次从哪开始选。
- 集合/组合问题的要求主要有两种:1、选出指定元素个数的组合(即包含指定数量元素的子集)2、选出元素和等于指定值的组合
1.1、元素无重+不可复选
子集问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| List<List<Integer>> ans; public List<List<Integer>> subsets(int[] nums) { ans = new ArrayList<>(); List<Integer> node = new ArrayList<>(); backtrace(nums, 0, node); return ans; }
private void backtrace(int[] nums, int start, List<Integer> node){ ans.add(new ArrayList<>(node));
for(int i = start; i < nums.length; i++){ node.add(nums[i]); backtrace(nums, i + 1, node); node.remove(new Integer(nums[i])); } }
|
1.2、元素可重+不可复选
包含重复元素的集合,选出子集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| List<List<Integer>> ans; public List<List<Integer>> subsets(int[] nums) { ans = new ArrayList<>(); List<Integer> node = new ArrayList<>(); Arrays.sort(nums); backtrace(nums, 0, node); return ans; }
private void backtrace(int[] nums, int start, List<Integer> node){
ans.add(new ArrayList<>(node));
for(int i = start; i < nums.length; i++){ if( i > start && nums[i] == nums[i - 1] ) continue; node.add(nums[i]); backtrace(nums, i + 1, node); node.remove(new Integer(nums[i])); } }
|
1.3、元素无重+可重复选
nums数组,指定 target,返回所有满足 元素和等于 target 的子序列
可以重复使用元素
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
| class Solution { List<List<Integer>> res; public List<List<Integer>> combinationSum(int[] candidates, int target) { res = new ArrayList<>(); List<Integer> one = new ArrayList<>(); Arrays.sort(candidates); backtrace(candidates, 0, one, target, 0); return res; }
private void backtrace(int[] nums, int start, List<Integer> one, int target, int cur_sum){ if(cur_sum == target){ res.add(new ArrayList<>(one)); return; }
for(int i = start; i < nums.length; i++){ if(cur_sum + nums[i] > target) break; one.add(nums[i]); backtrace(nums, i, one, target, cur_sum + nums[i]); one.remove(new Integer(nums[i])); } } }
|
2、排列问题
排列问题一般不存在对子序列元素和的要求,所以框架上略有不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| List<List<Integer>> res; public List<List<Integer>> solve(...){ res = new ArrayList<>(); List<Integer> one = new ArrayList<>(); backtrace(nums, one); return res; } private void backtrace(int[] nums, List<Integer> one){ if(one.size() == nums.length){ res.add(new ArrayList<>(one)); return; } for(int i = 0; i < nums.length; i++){ if( one.contains(nums[i]) ) continue; one.add(nums[i]); backtrace(nums, one); one.remove(new Integer(nums[i])); } }
|
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
| List<List<Integer>> res; boolean[] visited; public List<List<Integer>> solve(...){ res = new ArrayList<>(); List<Integer> one = new ArrayList<>(); visited = new boolean[nums.length]; backtrace(nums, one); return res; } private void backtrace(int[] nums, List<Integer> one){ if(one.size() == nums.length){ res.add(new ArrayList<>(one)); return; } for(int i = 0; i < nums.length; i++){
if( i > 0 && nums[i] == nums[i-1] && !visited[i-1] ) continue; if( visited[i] ) continue; visited[i] = true; one.add(nums[i]); backtrace(nums, one); visited[i] = false; one.remove(one.size() - 1); } }
|
岛屿问题
1、岛屿数量
200.
岛屿数量 - 力扣(LeetCode)
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
| class Solution { public int numIslands(char[][] grid) { int cnt = 0; int m = grid.length; int n = grid[0].length; for(int i = 0; i < m; i ++){ for(int j = 0; j < n; j++){ if(grid[i][j] == '0') continue; dfs(grid, i, j); cnt++; } } return cnt; }
private void dfs(char[][] grid, int i, int j){ int m = grid.length; int n = grid[0].length; if(i < 0 || i >= m || j < 0 || j >= n) return; if(grid[i][j] == '0') return; grid[i][j] = '0'; dfs(grid, i - 1, j); dfs(grid, i + 1, j); dfs(grid, i, j - 1); dfs(grid, i, j + 1); } }
|
2、封闭岛屿数量
封闭岛屿就是完全(上、下、左、右)被水包围的陆地
其实 封闭岛屿数量 = 200.岛屿数量 - 靠边岛屿数量
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
| class Solution { public int closedIsland(int[][] grid) { int m = grid.length; int n = grid[0].length; for(int j = 0; j < n; j++){ if(grid[0][j] == 0) dfs(grid, 0, j); if(grid[m-1][j] == 0) dfs(grid, m-1, j); } for(int i = 0; i < m; i++){ if(grid[i][0] == 0) dfs(grid, i, 0); if(grid[i][n-1] == 0) dfs(grid, i, n-1); } int cnt = 0;
for(int i = 0; i < m; i ++){ for(int j = 0; j < n; j ++){ if(grid[i][j] == 1) continue; dfs(grid, i, j); cnt++; } } return cnt; }
private void dfs(int[][] grid, int i, int j){ int m = grid.length; int n = grid[0].length; if(i < 0 || i >= m || j < 0 || j >= n) return; if(grid[i][j] == 1) return;
grid[i][j] = 1;
dfs(grid, i - 1, j); dfs(grid, i + 1, j); dfs(grid, i, j - 1); dfs(grid, i, j + 1);
} }
|
3、统计子岛屿
1905.
统计子岛屿 - 力扣(LeetCode)
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
| public int countSubIslands(int[][] grid1, int[][] grid2) { int m = grid1.length; int n = grid1[0].length; for(int i = 0; i < m; i++){ for(int j = 0; j < n; j++){ if(grid1[i][j] == 0 && grid2[i][j] == 1){ dfs(grid2, i, j); } } } int cnt = 0; for(int i = 0; i < m; i++){ for(int j = 0; j < n; j++){ if(grid2[i][j] == 1){ dfs(grid2, i, j); cnt++; } } } return cnt; }
private void dfs(int[][] grid, int i, int j){ int m = grid.length; int n = grid[0].length; if(i < 0 || i >= m || j < 0 || j >= n) return; if(grid[i][j] == 0) return;
grid[i][j] = 0;
dfs(grid, i-1, j); dfs(grid, i+1, j); dfs(grid, i, j-1); dfs(grid, i, j+1); }
|
4、不同岛屿的数量
统计不同形状的岛屿数量
这就要求在dfs遍历的过程中用某种方法记录岛屿的形状:在dfs过程中用字符进行拼接形成字符串
由于dfs的顺序是一样的、固定的,所以如果岛屿形状相同,开始dfs的位置肯定是一样的,最后形成的字符串也是一样的,再通过HashMap记录即可。
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
| public int numDistinctIslands(int[][] grid) { int m = grid.length; int n = grid[0].length;
Map<String, Integer> mapping = new HashMap<>(); for (int i = 0; i < m; i ++){ for (int j = 0; j < n; j++){ if(grid[i][j] == 1){ String shape = dfs(grid, i, j); System.out.println("(" + i + ", " + j + ")" + ": " + shape); if (!mapping.containsKey(shape)){ mapping.put(shape, 1); } } } }
return mapping.size(); }
private String dfs(int[][] grid, int i, int j){ int m = grid.length; int n = grid[0].length; if(i < 0 || i >= m || j < 0 || j >= n) return "0"; if (grid[i][j] == 0) return "0";
grid[i][j] = 0;
return "1" + dfs(grid, i - 1, j) + dfs(grid, i + 1, j) + dfs(grid, i, j - 1) + dfs(grid, i, j + 1); }
|
BFS算法
BFS算法是广度优先,遍历过程中,从开始节点到(第一次到)任一节点的的距离都是最小的
优点:BFS算法的普适性强,可以适用于所有情况
缺点:效率低,BFS的遍历由一个点等距离的向四周扩散
image-20221029210656103
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
| void bfs(Node root, Node target){ Queue<Node> queue = new LinkedList<>(); queue.offer(root); Set<Node> visited = new HashSet<>(); visited.add(root); int step = 0; while(!queue.isEmpty()){ int size = queue.size(); for(int i = 0; i < size; i++){ Node cur = queue.poll(); if(cur is target) return step; for(Node n : graph[n]){ if(!visited.contains(n)){ queue.offer(n); visited.add(n); } } } step ++; } return -1; }
|
Dijkstra算法
Dijkstra算法就是在BFS算法上的一个改进,针对有权重的图,主要的区别就在于存放节点的数据结构从普通的队列(Queue)变成了优先级队列(PriorityQueue),而使用优先级队列就可以按照代价(这个代价是从开始节点到当前节点的代价,所以Dijkstra算法是可以求出开始节点到所有节点的最小距离的)对节点进行排序,使得每次取出的节点都是代价最小的,从而实现第一次到某节点时的代价就是最小的。
Greedy Best-First Search
这个算法和Dijktra算法类似,但是这里所选取的代价是当前节点到target节点的理论代价。所以这个算法可以基本可以十分快速求出到达target节点的最小代价,特殊情况就是图中存在障碍物的情形,由于所用的代价是理论上的,并不是实际情况,所以会导致"走错路"的情况。
A*算法
Introduction to A*
A*算法有效地结合了Dijkstra算法和Greedy Best-First
Search算法,综合了两方面的代价,使得既拥有了上述两个算法的优势。
代码框架
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
| public Solution{
class AStar{
int f, g, h; Node cur;
public AStar(Node cur, int g, Node target){ this.cur = cur; this.h = getH(cur, target); this.g = g; this.f = this.g + this.h; }
public int getH(Node cur, Node target){
} } public int solve(Node start, Node target){ Queue<AStar> queue = new PriorityQueue<>((a, b) -> { return a.f - b.f; }); queue.offer(new AStar(start, 0, target)); Set<Node> visited = new HashSet<>(); visited.add(start); while(!queue.isEmpty()){ AStar node = queue.poll(); for(Node n : graph[node.cur]){ if(!visited.contains(n)){ if(n == target){ return node.g + 1; } queue.offer(new AStar(n, node.g + 1, target)); visited(n); } } } return -1; } }
|
应用
【773.滑动谜题】
773. 滑动谜题
- 力扣(LeetCode)
关键在于如何建模形成 graph 以及 node 是什么
在这道题中,就可以把\(2 \times
3\)的矩阵抽象成String
作为节点,而每次移动之后形成的节点作为该节点的邻居,这样就形成了graph