7、递归和回溯法
回溯算法解题套路框架
所有方法二实现思路:选择列表(排除不合法的选择 + 做选择 + 进入下一层回溯树 + 取消选择)、路径、结束条件
1、可以把「选择列表」和「路径」作为决策树的每个节点的属性
2、我们定义的「backtrack」函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性
3、每当走到树的底层叶子节点,其「路径」就是一个解
回溯算法秒杀所有排列 - 组合 - 子集问题
排列有序、组合无序:比如从 1 - 8 号球取 3 个球
1、排列:如果说一个一个拿,拿出来依次是 3 号、2 号、4 号,那么你拿出 234 和 324 是不一样的,这就是排列,有序的
2、组合:如果任意取 3 个球,由于 3 个一起取出,比如你取出的是 123 号球,不存在 123 和 321 有区别,都是这三个,这就是组合,无序的
1、什么是回溯
更多问题
93 - 复原 IP 地址
131 - 分割回文串
2、树形问题
2.1、方法一
public class Solution {
private static final String[] letterMap = {" ", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
private static final List<String> res = new ArrayList<>();
public static List<String> letterCombinations(String digits) {
res.clear();
if (digits == null || digits.equals("")) return res;
findCombination(digits, 0, "");
return res;
}
/**
* s 中保存了 digits[0 ... index - 1] 翻译得到的一个字母字符串
* 寻找和 digits[index] 匹配的字母, 获得 digits[0 ... index] 翻译得到的解
*/
private static void findCombination(String digits, int index, String s) {
if (index == digits.length()) {
res.add(s);
return;
}
char c = digits.charAt(index);
String letters = letterMap[c - '0'];
for (int i = 0; i < letters.length(); i++) {
findCombination(digits, index + 1, s + letters.charAt(i));
}
}
}
2.2、方法二
public class Solution {
private static final String[] letterMap = {" ", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
private static final List<String> res = new LinkedList<>();
public static List<String> letterCombinations(String digits) {
res.clear();
if (digits == null || digits.equals("")) return res;
StringBuilder track = new StringBuilder(); // 路径
backtrack(digits, 0, track);
return res;
}
/**
* 回溯算法框架
*/
private static void backtrack(String digits, int index, StringBuilder track) {
// 到达叶子节点, 将路径装入结果列表
if (track.length() == digits.length()) {
res.add(track.toString());
return;
}
char c = digits.charAt(index);
String letters = letterMap[c - '0'];
for (int i = 0; i < letters.length(); i++) {
track.append(letters.charAt(i)); // 做选择
backtrack(digits, index + 1, track); // 进入下一层回溯树
track.deleteCharAt(track.length() - 1); // 取消选择
}
}
}
3、回溯法是经典人工智能的基础
3.1、图示
3.2、实现
public class Solution {
private static boolean[] col; // 纵向
private static boolean[] dia1; // 对角线撇
private static boolean[] dia2; // 对角线捺
private static ArrayList<List<String>> res;
public static List<List<String>> solveNQueens(int n) {
res = new ArrayList<>();
col = new boolean[n]; // 纵向
dia1 = new boolean[2 * n - 1]; // 对角线撇
dia2 = new boolean[2 * n - 1]; // 对角线捺
LinkedList<Integer> row = new LinkedList<>(); // 路径
backtrack(n, 0, row);
return res;
}
/**
* 尝试在一个 n 皇后问题中, 摆放第 index 行的皇后位置
*/
private static void backtrack(int n, int index, LinkedList<Integer> row) {
if (index == n) {
res.add(generateBoard(n, row));
return;
}
for (int i = 0; i < n; i++) {
// 尝试将第 index 行的皇后摆放在第 i 列
if (!col[i] && !dia1[index + i] && !dia2[index - i + n - 1]) {
// 做选择
row.addLast(i);
col[i] = true;
dia1[index + i] = true;
dia2[index - i + n - 1] = true;
// 进入下一层回溯树
backtrack(n, index + 1, row);
// 取消选择
col[i] = false;
dia1[index + i] = false;
dia2[index - i + n - 1] = false;
row.removeLast();
}
}
}
private static List<String> generateBoard(int n, LinkedList<Integer> row) {
ArrayList<String> board = new ArrayList<>();
for (int i = 0; i < n; i++) {
char[] charArray = new char[n];
Arrays.fill(charArray, '.');
charArray[row.get(i)] = 'Q';
board.add(new String(charArray));
}
return board;
}
}
3.3、更多
4、排列问题
更多问题
47 - 全排列 II
4.1、方法一
public class Solution {
private static final List<List<Integer>> res = new ArrayList<>();
private static boolean[] used;
public static List<List<Integer>> permute(int[] nums) {
res.clear();
if (nums == null || nums.length == 0) return res;
used = new boolean[nums.length];
Arrays.fill(used, false);
generatePermutation(nums, 0, new ArrayList<>());
return res;
}
/**
* p 中保存了一个有 index 个元素的排列
* 向这个排列的末尾添加第 index + 1 个元素, 获得一个有 index + 1 个元素的排列
*/
private static void generatePermutation(int[] nums, int index, ArrayList<Integer> p) {
if (index == nums.length) {
res.add(new ArrayList<>(p));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
p.add(nums[i]);
used[i] = true;
generatePermutation(nums, index + 1, p);
p.remove(p.size() - 1);
used[i] = false;
}
}
}
4.2、方法二
public class Solution {
private static final List<List<Integer>> res = new ArrayList<>();
private static boolean[] used;
public static List<List<Integer>> permute(int[] nums) {
res.clear();
if (nums == null || nums.length == 0) return res;
used = new boolean[nums.length];
LinkedList<Integer> track = new LinkedList<>(); // 路径
backtrack(nums, track);
return res;
}
/**
* 回溯算法框架
*/
private static void backtrack(int[] nums, LinkedList<Integer> track) {
// 到达叶子节点, 将路径装入结果列表
if (track.size() == nums.length) {
res.add(new LinkedList<>(track));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue; // 排除不合法的选择
track.add(nums[i]); // 做选择
used[i] = true;
backtrack(nums, track); // 进入下一层回溯树
track.removeLast(); // 取消选择
used[i] = false;
}
}
}
5、组合问题
5.1、方法一
public class Solution {
private static final List<List<Integer>> res = new ArrayList<>();
public static List<List<Integer>> combine(int n, int k) {
res.clear();
LinkedList<Integer> track = new LinkedList<>();
generateCombinations(n, k, 1, track);
return res;
}
/**
* 求解 C(n, k), 当前已经找到的组合存储在 track 中, 需要从 start 开始搜索新的元素
*/
private static void generateCombinations(int n, int k, int start, LinkedList<Integer> track) {
if (track.size() == k) {
res.add(new LinkedList<>(track));
return;
}
for (int i = start; i <= n; i++) {
track.add(i);
generateCombinations(n, k, i + 1, track);
track.removeLast();
}
}
}
5.2、方法二
public class Solution {
private static final List<List<Integer>> res = new LinkedList<>();
private static final LinkedList<Integer> track = new LinkedList<>(); // 路径
public static List<List<Integer>> combine(int n, int k) {
res.clear();
track.clear();
backtrack(1, n, k);
return res;
}
/**
* 回溯算法框架
*/
private static void backtrack(int start, int n, int k) {
// 到达叶子节点, 将路径装入结果列表
if (k == track.size()) {
res.add(new LinkedList<>(track));
return;
}
for (int i = start; i <= n; i++) {
track.addLast(i); // 做选择
backtrack(i + 1, n, k); // 进入下一层回溯树
track.removeLast(); // 取消选择
}
}
}
6、组合问题的优化
更多问题
39 - 组合总和
40 - 组合总和 II
216 - 组合总和 III
78 - 子集
90 - 子集 II
401 - 二进制手表
6.1、方法一
public class Solution {
private static final List<List<Integer>> res = new ArrayList<>();
public static List<List<Integer>> combine(int n, int k) {
res.clear();
LinkedList<Integer> track = new LinkedList<>(); // 路径
generateCombinations(n, k, 1, track);
return res;
}
/**
* 求解 C(n, k), 当前已经找到的组合存储在 track 中, 需要从 start 开始搜索新的元素
*/
private static void generateCombinations(int n, int k, int start, LinkedList<Integer> track) {
if (track.size() == k) {
res.add(new LinkedList<>(track));
return;
}
// 还有 k - track.size() 个空位, 所以 [i ... n] 中至少要有 k - track.size() 个元素
// 因此 i <= n - (k - track.size() - 1)
for (int i = start; i <= n - (k - track.size() - 1); i++) {
track.add(i);
generateCombinations(n, k, i + 1, track);
track.removeLast();
}
}
}
6.2、方法二
public class Solution {
private static final List<List<Integer>> res = new ArrayList<>();
private static final LinkedList<Integer> track = new LinkedList<>(); // 路径
public static List<List<Integer>> combine(int n, int k) {
res.clear();
track.clear();
backtrack(1, n, k);
return res;
}
private static void backtrack(int start, int n, int k) {
// 到达叶子节点, 将路径装入结果列表
if (k == track.size()) {
res.add(new LinkedList<>(track));
return;
}
// 还有 k - track.size() 个空位, 所以 [i ... n] 中至少要有 k - track.size() 个元素
// 因此 i <= n - (k - track.size() - 1)
for (int i = start; i <= n - (k - track.size() - 1); i++) {
track.addLast(i); // 做选择
backtrack(i + 1, n, k); // 进入下一层回溯树
track.removeLast(); // 取消选择
}
}
}
7、floodfill 算法
public class Solution {
private static int dir[][] = {
{0, 1}, {1, 0}, {0, -1}, {-1, 0}
};
private static int m, n; // m - 1 行 n - 1 列
private static boolean visited[][];
public static int numIslands(char[][] grid) {
if (grid == null || grid.length == 0 || grid[0].length == 0) return 0;
m = grid.length;
n = grid[0].length;
visited = new boolean[m][n];
int res = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1' && !visited[i][j]) {
res++;
dfs(grid, i, j);
}
}
}
return res;
}
/**
* 从 grid[x][y] 的位置开始, 进行 floodfill
* 保证 (x, y) 合法, 且 grid[x][y] 是没有被访问过的陆地
*/
private static void dfs(char[][] grid, int x, int y) {
visited[x][y] = true;
for (int i = 0; i < 4; i++) {
int newx = x + dir[i][0];
int newy = y + dir[i][1];
if (inArea(newx, newy) && !visited[newx][newy] && grid[newx][newy] == '1') dfs(grid, newx, newy);
}
}
private static boolean inArea(int x, int y) {
return x >= 0 && x < m && y >= 0 && y < n;
}
}
8、二维平面上的回溯法
public class Solution {
// 上右下左
private static int dir[][] = {
{-1, 0}, {0, 1}, {1, 0}, {0, -1}
};
private static int m, n; // m - 1 行 n - 1 列
private static boolean[][] visited;
public static boolean exist(char[][] board, String word) {
m = board.length;
n = board[0].length;
visited = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (searchWord(board, word, 0, i, j)) return true;
}
}
return false;
}
/**
* 从 board[startx][starty] 开始, 寻找 word[index ... word.length)
*/
private static boolean searchWord(char[][] board, String word, int index, int startx, int starty) {
if (index == word.length() - 1) return board[startx][starty] == word.charAt(index);
if (board[startx][starty] == word.charAt(index)) {
visited[startx][starty] = true; // 做选择
// 从 (startx, starty) 出发, 向四个方向寻找
for (int i = 0; i < 4; i++) {
int newx = startx + dir[i][0];
int newy = starty + dir[i][1];
if (inArea(newx, newy) && !visited[newx][newy] && searchWord(board, word, index + 1, newx, newy))
return true;
}
visited[startx][starty] = false; // 取消选择
}
return false;
}
private static boolean inArea(int x, int y) {
return x >= 0 && x < m && y >= 0 && y < n;
}
}
本文来自博客园,作者:lidongdongdong~,转载请注明原文链接:https://www.cnblogs.com/lidong422339/p/17411526.html