文章目录
1.回溯模版
List result = new ArrayList();
public void backtrack(路径, 选择列表){
if(满足结束条件){
result.add(路径);
return;
}
for(选择 : 选择列表){
做选择;
backtrack(路径, 选择列表); //递归遍历
撤销选择;
}
}
子集:所有节点
i
从start
开始,因为不可以往前选
组合:某一层的节点
i
从start
开始,因为不可以往前选- 也可以看成某一层的子集,所以相比子集
res
添加path
时要加if
判断
排列:叶子节点
i
从0
开始,因为可以往前选
2.三种形式总结
精简总结:
- 无重可复选最简单,什么也不用管
- 可重要剪枝,排序后和前一个相同要跳过,排列前一个还必须要使用过
- 不可复选组合/子集下次递归从
i+1
开始,排列加used
数组判断是否选过
详细总结:
- 可重/不可重
- 元素可重那就会有重复,那就要剪枝
- 对于子集/组合,要加排序和
nums[i] == nums[i - 1]
判断 - 排列的可重还要加上
!used[i - 1]
才能剪枝,这样才能保证元素的相对位置不变
- 可复选/不可复选
- 不可复选意味着必须选没用过的,那怎么判断用没用过呢?
- 对于子集/组合
- 可复选下次递归就从当前开始,那
backtarck
函数的参数是i
就行,这样下次还能选当前元素 - 不可复选那每次必须选后面的元素,所以
backtarck
函数的参数是i+1
- 可复选下次递归就从当前开始,那
- 排列:排列递归参数没有
i
,要加used
数组判断是否选过
形式一 :元素无重可复选
元素无重可复选,即 nums
中的元素都是唯一的,每个元素可以被使用若干次,只要删掉去重逻辑即可,backtrack
核心代码如下:
/* 组合/子集问题回溯算法框架 */
void backtrack(int[] nums, int start) {
// i 从 start 开始
for (int i = start; i < nums.length; i++) {
path.addLast(nums[i]);
// 注意参数是 i
backtrack(nums, i);
path.removeLast();
}
}
/* 排列问题回溯算法框架 */
void backtrack(int[] nums) {
// i 从 0 开始
for (int i = 0; i < nums.length; i++) {
path.add(nums[i]);
backtrack(nums);
path.removeLast();
}
}
形式二 :元素无重不可复选
元素无重不可复选,即 nums
中的元素都是唯一的,每个元素最多只能被使用一次,backtrack
核心代码如下:
/* 组合/子集问题回溯算法框架 */
void backtrack(int[] nums, int start) {
// i 从 start 开始
for (int i = start; i < nums.length; i++) {
path.add(nums[i]);
// 注意参数是 i+1
backtrack(nums, i + 1);
path.removeLast();
}
}
/* 排列问题回溯算法框架 */
void backtrack(int[] nums) {
// i 从 0 开始
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) {
// i 从 start 开始
for (int i = start; i < nums.length; i++) {
// 剪枝逻辑,跳过值相同的相邻树枝
if (i > start && nums[i] == nums[i - 1]) {
continue;
}
track.addLast(nums[i]);
// 注意参数是 i+1
backtrack(nums, i + 1);
track.removeLast();
}
}
/* 排列问题回溯算法框架 */
Arrays.sort(nums);
void backtrack(int[] nums) {
// i 从 0 开始
for (int i = 0; i < nums.length; i++) {
// 剪枝逻辑
if (used[i]) {
continue;
}
// 剪枝逻辑,固定相同的元素在排列中的相对位置
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
used[i] = true;
track.addLast(nums[i]);
backtrack(nums);
track.removeLast();
used[i] = false;
}
}
3.组合
3-77.组合🟡
题目:代码:给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
链接:77. 组合
代码:
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
backtrack(n, k, 1);
return result;
}
// 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex
private void backtrack(int n, int k, int startIndex) {
//终止条件
if (path.size() == k) {
// new一个对象从而不影响后续path的值
result.add(new ArrayList<>(path));
return;
}
// for (int i =startIndex;i<=n;i++){的剪枝版本
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
path.add(i);
backtrack(n, k, i + 1);
path.removeLast();
}
}
}
4-216.组合总和 III🟡
题目:找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
代码:
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backtrack(n, k, 1, 0);
return result;
}
private void backtrack(int targetSum, int k, int startIndex, int sum) {
// 减枝
if (sum > targetSum) {
return;
}
if (path.size() == k) {
if (sum == targetSum)
result.add(new ArrayList<>(path));
return;
}
// 减枝 9 - (k - path.size()) + 1
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
path.add(i);
sum += i;
backtrack(targetSum, k, i + 1, sum);
// 回溯
path.removeLast();
sum -= i;
}
}
}
5-17.电话号码的字母组合🟡
题目:给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
代码:
class Solution {
// 每个数字到字母的映射
String[] mapping = new String[]{
"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
};
List<String> res = new LinkedList<>();
public List<String> letterCombinations(String digits) {
if (digits.isEmpty()) {
return res;
}
// 从 digits[0] 开始进行回溯
backtrack(digits, 0, new StringBuilder());
return res;
}
// 回溯算法主函数
void backtrack(String digits, int start, StringBuilder sb) {
if (sb.length() == digits.length()) {
// 到达回溯树底部
res.add(sb.toString());
return;
}
// 回溯算法框架
for (int i = start; i < digits.length(); i++) {
int digit = digits.charAt(i) - '0';
for (char c : mapping[digit].toCharArray()) {
// 做选择
sb.append(c);
// 递归下一层回溯树
backtrack(digits, i + 1, sb);
// 撤销选择
sb.deleteCharAt(sb.length() - 1);
}
}
}
}
7-39.组合总和🟡
题目:给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
链接:39. 组合总和
代码:
class Solution {
public List<List<Integer>> res = new ArrayList<>();
public LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
backtrack(candidates, target, 0, 0);
return res;
}
public void backtrack(int[] candidates, int target, int sum, int index) {
if (sum == target) {
res.add(new ArrayList<>(path));
return;
}
// 在循环时进行剪枝,需要对数组进行排序
for (int i = index; i < candidates.length && sum + candidates[i] <= target; i++) {
sum += candidates[i];
path.add(candidates[i]);
backtrack(candidates, target, sum, i);
sum -= candidates[i];
path.removeLast();
}
}
}
8-40. 组合总和 II🟡
题目:给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
注意: 解集不能包含重复的组合。
链接:40. 组合总和 II
代码:
class Solution {
public List<List<Integer>> res = new ArrayList<>();
public LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
// 为了将重复的数字都放到一起,所以先进行排序
Arrays.sort(candidates);
backtrack(candidates, target, 0, 0);
return res;
}
public void backtrack(int[] candidates, int target, int sum, int index) {
if (sum == target) {
res.add(new ArrayList<>(path));
return;
}
for (int i = index; i < candidates.length && sum + candidates[i] <= target; i++) {
// 跳过同一树层使用过的元素
if (i > index && candidates[i] == candidates[i - 1])
continue;
sum += candidates[i];
path.add(candidates[i]);
// 每个元素只能出现一次,所以是i+1
backtrack(candidates, target, sum, i + 1);
sum -= path.get(path.size() - 1);
path.removeLast();
}
}
}
9-131.分割回文串🟡
题目:给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
链接:131. 分割回文串
代码:
class Solution {
List<List<String>> res = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();
public List<List<String>> partition(String s) {
backtrack(s, 0);
return res;
}
private void backtrack(String s, int start) {
if (start >= s.length()) {
// 如果起始位置大于s的大小,说明找到了一组分割方案
res.add(new ArrayList<String>(path));
return;
}
for (int i = start; i < s.length(); i++) {
if (!isPalindrome(s, start, i)) {
// s[start..i] 不是回文串,不能分割
continue;
}
// s[start..i] 是一个回文串,可以进行分割str
// 做选择,把 s[start..i] 放入路径列表中
path.addLast(s.substring(start, i + 1));
// 进入回溯树的下一层,继续切分 s[i+1..]
backtrack(s, i + 1);
// 撤销选择
path.removeLast();
}
}
// 用双指针技巧判断 s[start..end] 是否是一个回文串
private boolean isPalindrome(String s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) {
return false;
}
}
return true;
}
}
10-93.复原 IP 地址🟡
题目:有效 IP 地址 正好由四个整数(每个整数位于 0
到 255
之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
例如:"0.1.2.201"
和 "192.168.1.1"
是 有效 IP 地址,但是 "0.011.255.245"
、"192.168.1.312"
和 "192.168@1.1"
是 无效 IP 地址。
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s
中插入 '.'
来形成。你 不能 重新排序或删除 s
中的任何数字。你可以按 任何 顺序返回答案。
链接:93. 复原 IP 地址
代码:
class Solution {
List<String> res = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();
public List<String> restoreIpAddresses(String s) {
backtrack(s, 0);
return res;
}
void backtrack(String s, int start) {
// 即整个 s 被成功分割为合法的四部分,记下答案
if (start == s.length() && path.size() == 4) {
// join函数直接用.拼接path
res.add(String.join(".", path));
}
for (int i = start; i < s.length(); i++) {
if (!isValid(s, start, i)) {
continue;
}
path.addLast(s.substring(start, i + 1));
backtrack(s, i + 1);
path.removeLast();
}
}
boolean isValid(String s, int start, int end) {
// 位数大于3
if (end - start + 1 > 3) {
return false;
}
if (s.charAt(start) == '0' && start != end) {
return false;
}
int i = Integer.parseInt(s.substring(start, end + 1));
return i <= 255;
}
}
4.子集
11-78.子集🟡
题目:给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
链接:78. 子集
代码:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums, 0);
return res;
}
public void backtrack(int[] nums, int start) {
// 不是只记录叶子节点,而是记录所有节点,所以不用if判断
res.add(new LinkedList<>(path));
// 终止条件可不加
if (start >= nums.length) {
return;
}
for (int i = start; i < nums.length; i++) {
path.add(nums[i]);
backtrack(nums, i + 1);
path.removeLast();
}
}
}
13-90.子集 II🟡
题目:给你一个整数数组 nums
,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
链接:90. 子集 II
代码:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
// 先排序,让相同的元素靠在一起
Arrays.sort(nums);
backtrack(nums, 0);
return res;
}
private void backtrack(int[] nums, int start) {
// 前序位置,每个节点的值都是一个子集
res.add(new ArrayList<>(path));
for (int i = start; i < nums.length; i++) {
// 跳过当前树层使用过的、相同的元素
if (i > start && nums[i - 1] == nums[i]) {
continue;
}
path.add(nums[i]);
backtrack(nums, i + 1);
path.removeLast();
}
}
}
14-491.非递减子序列🟡
题目:给你一个整数数组 nums
,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
链接:491. 非递减子序列
代码:
class Solution {
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backtrack(nums, 0);
return res;
}
void backtrack(int[] nums, int start) {
if (path.size() >= 2) {
res.add(new LinkedList<>(path));
}
// 用哈希集合防止重复选择相同元素
// 题目说明-100 <= nums[i] <= 100,所以也可以用一个used数组去重
// int[] used = new int[201];
HashSet<Integer> used = new HashSet<>();
for (int i = start; i < nums.length; i++) {
// 保证集合中元素都是递增顺序
if (!path.isEmpty() && path.getLast() > nums[i]) {
continue;
}
// 保证不要重复使用相同的元素
if (used.contains(nums[i])) {
continue;
}
used.add(nums[i]);
path.add(nums[i]);
backtrack(nums, i + 1);
path.removeLast();
}
}
}
5.排列
15-46.全排列🟡
题目:给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
链接:46. 全排列
代码:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
boolean[] used = new boolean[nums.length];
backtrack(nums, used);
return res;
}
private void backtrack(int[] nums, boolean[] used) {
// 到达叶子节点
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
// 注意从0开始,可以往回选
// [1,2]中可以选择[1,2]也可以是[2,1]
for (int i = 0; i < nums.length; i++) {
// 因为从0开始,所以会重复选择元素,用used去重
if (used[i])
continue;
path.add(nums[i]);
used[i] = true;
backtrack(nums, used);
path.removeLast();
used[i] = false;
}
}
}
16-47.全排列 II🟡
题目:给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
链接:47. 全排列 II
代码:
// 与上一题全排列的区别是本题有重复数据
class Solution {
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
// 先排序,让相同的元素靠在一起
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
backtrack(nums, used);
return res;
}
void backtrack(int[] nums, boolean[] used) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) {
continue;
}
// 固定相同的元素在排列中的相对位置,前一个元素必须使用过
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
path.add(nums[i]);
used[i] = true;
backtrack(nums, used);
path.removeLast();
used[i] = false;
}
}
}
6.棋盘
20-51.N 皇后🔴
题目:按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
链接:51. N 皇后
代码:
// 棋盘是个n×n的数组,因此可以将第i行看成树的第i层
// 第j列看成树的第j个分支
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
// 填充棋盘元素为.
char[][] chessboard = new char[n][n];
for (char[] cArr : chessboard) {
Arrays.fill(cArr, '.');
}
backtrack(n, chessboard, 0);
return res;
}
public void backtrack(int n, char[][] chessboard, int row) {
if (row == n) {
// 数组转列表
List<String> sList = new ArrayList<>();
for (char[] cArr : chessboard) {
sList.add(String.valueOf(cArr));
}
res.add(sList);
}
for (int col = 0; col < n; col++) {
if (isValid(n, chessboard, row, col)) {
chessboard[row][col] = 'Q';
backtrack(n, chessboard, row + 1);
chessboard[row][col] = '.';
}
}
}
public boolean isValid(int n, char[][] chessboard, int row, int col) {
// 检查列
for (int i = 0; i < row; i++) {
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查左上角
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查右上角
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
}
21-37.解数独🔴
题目:编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
- 数字
1-9
在每一行只能出现一次。 - 数字
1-9
在每一列只能出现一次。 - 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.'
表示。
链接:37. 解数独
代码:
// 从根部一直往下找符合条件的叶子节点,不行就回溯
// 若找到一条符合条件的路径,立刻返回
class Solution {
public void solveSudoku(char[][] board) {
backtrack(board);
}
// 找到符合条件的值就立即返回,因此回溯需要有boolean返回值
public boolean backtrack(char[][] board) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.')
continue;
for (char k = '1'; k <= '9'; k++) {
// 判断 board[i][j] 是否可以填入 k
if (isValid(i, j, k, board)) {
board[i][j] = k;
// 如果找到数独的一组解立刻返回
if (backtrack(board))
return true;
board[i][j] = '.';
}
}
// 当前的空白格不能填下任何一个数字,返回false进行回溯
return false;
}
}
// 遍历完棋盘也没返回false,说明找到了一组解
return true;
}
// 判断 board[i][j] 是否可以填入 k
public boolean isValid(int row, int col, char k, char[][] board) {
for (int i = 0; i < 9; i++) {
// 判断行是否存在重复
if (board[row][i] == k)
return false;
// 判断列是否存在重复
if (board[i][col] == k)
return false;
}
// 判断 3 x 3 方框是否存在重复
int boxRow = (row / 3) * 3; // 方框的起始行索引
int boxCol = (col / 3) * 3; // 方框的起始列索引
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (board[boxRow + i][boxCol + j] == k) {
return false;
}
}
}
return true;
}
}