result = [] def backtrack(路径, 选择列表): if 满足结束条件: result.add(路径) return for 选择 in 选择列表: 做选择 backtrack(路径, 选择列表) 撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,这其实和多叉树的遍历很像:
void traverse(TreeNode root) { for (TreeNode child : root.childern) // 前序遍历需要的操作 traverse(child); // 后序遍历需要的操作 }
46. 全排列 - 力扣(LeetCode) (leetcode-cn.com)
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案
List<List<Integer>> res = new LinkedList<>(); /* 主函数,输入一组不重复的数字,返回它们的全排列 */ List<List<Integer>> permute(int[] nums) { // 记录「路径」 LinkedList<Integer> track = new LinkedList<>(); // 「路径」中的元素会被标记为 true,避免重复使用,初始化为false boolean[] used = new boolean[nums.length]; backtrack(nums, track, used); return res; } // 路径:记录在 track 中 // 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false) // 结束条件:nums 中的元素全都在 track 中出现 void backtrack(int[] nums, LinkedList<Integer> track, boolean[] used) { // 触发结束条件 if (track.size() == nums.length) { res.add(new LinkedList(track)); return; } for (int i = 0; i < nums.length; i++) { // 排除不合法的选择 if (used[i]) { // nums[i] 已经在 track 中,跳过 continue; } // 做选择 track.add(nums[i]); used[i] = true; // 进入下一层决策树 backtrack(nums, track, used); // 取消选择 track.removeLast(); used[i] = false; } }
47. 全排列 II - 力扣(LeetCode) (leetcode-cn.com)
为上一题的进阶,给定数组有重复,要解决重复问题,我们只要设定一个规则,保证在填第 i个数的时候重复数字只会被填入一次即可。而在本题解中,我们选择对原数组排序,保证相同的数字都相邻,然后每次填入的数一定是这个数所在重复数集合中「从左往右第一个未被填过的数字」,只需将上述代码稍加改动即可,代码如下:
List<List<Integer>> res = new LinkedList<>(); public List<List<Integer>> permute(int[] nums) { if (nums.length == 0) return res; LinkedList<Integer> track = new LinkedList<>(); boolean[] used = new boolean[nums.length]; // 先将数组排序 Arrays.sort(nums); backtrack(nums, used, track); return res; } void backtrack(int[] nums, boolean[] used, LinkedList<Integer> track){ if (nums.length == track.size()){ res.add(new LinkedList<>(track)); return; } for (int i = 0; i < nums.length; i++) { // 这里条件增加 if (used[i] == true || (i > 0 && nums[i] == nums[i-1] && used[i-1] == true)) continue; used[i] = true; track.add(nums[i]); backtrack(nums, used, track); used[i] = false; track.removeLast(); } return; }
78. 子集 - 力扣(LeetCode) (leetcode-cn.com)
List<List<Integer>> res = new LinkedList<>(); // 记录回溯算法的递归路径 LinkedList<Integer> track = new LinkedList<>(); // 主函数 public List<List<Integer>> subsets(int[] nums) { backtrack(nums, 0); return res; } // 回溯算法核心函数,遍历子集问题的回溯树 void backtrack(int[] nums, int start) { // 前序位置,每个节点的值都是一个子集 res.add(new LinkedList<>(track)); // 回溯算法标准框架 for (int i = start; i < nums.length; i++) { // 做选择 track.addLast(nums[i]); // 通过 start 参数控制树枝的遍历,避免产生重复的子集 backtrack(nums, i + 1); // 撤销选择 track.removeLast(); } }
90. 子集 II - 力扣(LeetCode) (leetcode-cn.com)
List<List<Integer>> res = new LinkedList<>(); LinkedList<Integer> track = new LinkedList<>(); public List<List<Integer>> subsetsWithDup(int[] nums) { // 先排序,让相同的元素靠在一起 Arrays.sort(nums); backtrack(nums, 0); return res; } void backtrack(int[] nums, int start) { // 前序位置,每个节点的值都是一个子集 res.add(new LinkedList<>(track)); for (int i = start; i < nums.length; i++) { // 剪枝逻辑,值相同的相邻树枝,只遍历第一条 if (i > start && nums[i] == nums[i - 1]) { continue; } track.addLast(nums[i]); backtrack(nums, i + 1); track.removeLast(); } }
77. 组合 - 力扣(LeetCode) (leetcode-cn.com)
List<List<Integer>> res = new LinkedList<>(); // 记录回溯算法的递归路径 LinkedList<Integer> track = new LinkedList<>(); // 主函数 public List<List<Integer>> combine(int n, int k) { backtrack(1, n, k); return res; } void backtrack(int start, int n, int k) { // base case if (k == track.size()) { // 遍历到了第 k 层,收集当前节点的值 res.add(new LinkedList<>(track)); return; } // 回溯算法标准框架 for (int i = start; i <= n; i++) { // 选择 track.addLast(i); // 通过 start 参数控制树枝的遍历,避免产生重复的子集 backtrack(i + 1, n, k); // 撤销选择 track.removeLast(); } }
40. 组合总和 II - 力扣(LeetCode) (leetcode-cn.com)
List<List<Integer>> res = new LinkedList<>(); // 记录回溯的路径 LinkedList<Integer> track = new LinkedList<>(); // 记录 track 中的元素之和 int trackSum = 0; public List<List<Integer>> combinationSum2(int[] candidates, int target) { if (candidates.length == 0) { return res; } // 先排序,让相同的元素靠在一起 Arrays.sort(candidates); backtrack(candidates, 0, target); return res; } // 回溯算法主函数 void backtrack(int[] nums, int start, int target) { // base case,达到目标和,找到符合条件的组合 if (trackSum == target) { res.add(new LinkedList<>(track)); return; } // base case,超过目标和,直接结束 if (trackSum > target) { return; } // 回溯算法标准框架 for (int i = start; i < nums.length; i++) { // 剪枝逻辑,值相同的树枝,只遍历第一条 if (i > start && nums[i] == nums[i - 1]) { continue; } // 做选择 track.add([i]); trackSum += nums[i]; // 递归遍历下一层回溯树 backtrack(nums, i + 1, target); // 撤销选择 track.removeLast(); trackSum -= nums[i]; } }
39. 组合总和 - 力扣(LeetCode) (leetcode-cn.com)
List<List<Integer>> res = new LinkedList<>(); // 记录回溯的路径 LinkedList<Integer> track = new LinkedList<>(); // 记录 track 中的路径和 int trackSum = 0; public List<List<Integer>> combinationSum(int[] candidates, int target) { if (candidates.length == 0) { return res; } backtrack(candidates, 0, target); return res; } // 回溯算法主函数 void backtrack(int[] nums, int start, int target) { // base case,找到目标和,记录结果 if (trackSum == target) { res.add(new LinkedList<>(track)); return; } // base case,超过目标和,停止向下遍历 if (trackSum > target) { return; } // 回溯算法标准框架 for (int i = start; i < nums.length; i++) { // 选择 nums[i] trackSum += nums[i]; track.add(nums[i]); // 递归遍历下一层回溯树 // 同一元素可重复使用,注意参数 backtrack(nums, i, target); // 撤销选择 nums[i] trackSum -= nums[i]; track.removeLast(); } }
其实和全排列类似,直接套回溯的框架:
vector<vector<string>> res; /* 输入棋盘边长 n,返回所有合法的放置 */ vector<vector<string>> solveNQueens(int n) { // '.' 表示空,'Q' 表示皇后,初始化空棋盘。 vector<string> board(n, string(n, '.')); backtrack(board, 0); return res; } // 路径:board 中小于 row 的那些行都已经成功放置了皇后 // 选择列表:第 row 行的所有列都是放置皇后的选择 // 结束条件:row 超过 board 的最后一行 void backtrack(vector<string>& board, int row) { // 触发结束条件 if (row == board.size()) { res.push_back(board); return; } int n = board[row].size(); for (int col = 0; col < n; col++) { // 排除不合法选择 if (!isValid(board, row, col)) { continue; } // 做选择 board[row][col] = 'Q'; // 进入下一行决策 backtrack(board, row + 1); // 撤销选择 board[row][col] = '.'; } } // 不需要检查左下角,右下角和下方的格子,因为是从上往下放。只需要检查左上角,右上角和上方的格子。 bool isValid(vector<string>& board, int row, int col) { int n = board.size(); // 检查列是否有皇后互相冲突 for (int i = 0; i < n; i++) { if (board[i][col] == 'Q') return false; } // 检查右上方是否有皇后互相冲突 for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { if (board[i][j] == 'Q') return false; } // 检查左上方是否有皇后互相冲突 for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) { if (board[i][j] == 'Q') return false; } return true; }
如果只要求出其中一个解,修改为:
bool isValid(vector<string>& board, int row, int col) { int n = board.size(); // 检查列是否有皇后互相冲突 for (int i = 0; i < n; i++) { if (board[i][col] == 'Q') return false; } // 检查右上方是否有皇后互相冲突 for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { if (board[i][j] == 'Q') return false; } // 检查左上方是否有皇后互相冲突 for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) { if (board[i][j] == 'Q') return false; } return true; }
由于子集问题和组合问题本质上是一样的,无非就是 base case 有一些区别
形式一、元素无重不可复选,即 nums
中的元素都是唯一的,每个元素最多只能被使用一次,backtrack
核心代码如下:
/* 组合/子集问题回溯算法框架 */ void backtrack(int[] nums, int start) { // 回溯算法标准框架 for (int i = start; i < nums.length; i++) { // 做选择 track.addLast(nums[i]); // 注意参数 backtrack(nums, i + 1); // 撤销选择 track.removeLast(); } } /* 排列问题回溯算法框架 */ void backtrack(int[] nums) { for (int i = 0; i < nums.length; i++) { // 剪枝逻辑 if (used[i]) { continue; } // 做选择 used[i] = true; track.addLast(nums[i]); backtrack(nums); // 取消选择 track.removeLast(); used[i] = false; } }
形式二、元素可重不可复选,即 nums
中的元素可以存在重复,每个元素最多只能被使用一次,其关键在于排序和剪枝,backtrack
核心代码如下:
Arrays.sort(nums); /* 组合/子集问题回溯算法框架 */ void backtrack(int[] nums, int start) { // 回溯算法标准框架 for (int i = start; i < nums.length; i++) { // 剪枝逻辑,跳过值相同的相邻树枝 if (i > start && nums[i] == nums[i - 1]) { continue; } // 做选择 track.addLast(nums[i]); // 注意参数 backtrack(nums, i + 1); // 撤销选择 track.removeLast(); } } Arrays.sort(nums); /* 排列问题回溯算法框架 */ void backtrack(int[] nums) { for (int i = 0; i < nums.length; i++) { // 剪枝逻辑 if (used[i]) { continue; } // 剪枝逻辑,固定相同的元素在排列中的相对位置 if (used[i] == true || (i > 0 && nums[i] == nums[i-1] && used[i-1] == true)) { continue; } // 做选择 used[i] = true; track.addLast(nums[i]); backtrack(nums); // 取消选择 track.removeLast(); used[i] = false; } }
形式三、元素无重可复选,即 nums
中的元素都是唯一的,每个元素可以被使用若干次,只要删掉去重逻辑即可,backtrack
核心代码如下:
/* 组合/子集问题回溯算法框架 */ void backtrack(int[] nums, int start) { // 回溯算法标准框架 for (int i = start; i < nums.length; i++) { // 做选择 track.addLast(nums[i]); // 注意参数 backtrack(nums, i); // 撤销选择 track.removeLast(); } } /* 排列问题回溯算法框架 */ void backtrack(int[] nums) { for (int i = 0; i < nums.length; i++) { // 做选择 track.addLast(nums[i]); backtrack(nums); // 取消选择 track.removeLast(); } }