最近把LeetCode上的backTracking的题目做了一下,发现都是一个套路~
backTracking链接:https://leetcode.com/tag/backtracking/
还有几道比较难的Medium的题和Hard的题没做出来,后面会继续更新和加详细解法解释~
回溯backtracking的公式:
def backtrack(state: State, choices: list[choice], res: list[state]):
"""回溯算法框架"""
# 判断是否为解
if is_solution(state):
# 记录解
record_solution(state, res)
# 不再继续搜索
return
# 遍历所有选择
for choice in choices:
# 剪枝:判断选择是否合法
if is_valid(state, choice):
# 尝试:做出选择,更新状态
make_choice(state, choice)
backtrack(state, choices, res)
# 回退:撤销选择,恢复到之前的状态
undo_choice(state, choice)
文章目录
- 78. Subsets 回溯的入门之子集
- 90. Subsets II
- 46. Permutations 全排列 注意(全排列的每一种可能都是需要从nums的第一个元素开始遍历的)
- 77.
- 39. Combination Sum 组合总和 着重看看这个 (3剪枝的具体位置,和需要start(因为结果不能重复))
- 40. Combination Sum II 组合总和 II 因为元素只能取一次,所以排序,i+1和跳过相等元素
- 216. Combination Sum III
- 784. Letter Case Permutation
- 17. Letter Combinations of a Phone Number
- 22. Generate Parentheses 括号生成器
- 131. Palindrome Partitioning 分割回文字串
- 52. N-Queens II N皇后
- 51. N-Queens n皇后
- 526. Beautiful Arrangement 漂亮排列
- 401. Binary Watch
78. Subsets 回溯的入门之子集
返回数组的所有子集。
这种题目都是使用这个套路,就是用一个循环去枚举当前所有情况,然后把元素加入,递归,再把元素移除
按照这个套路来做,可以解决backTracking的问题
list.add(nums[i]); // 第三步 元素加入临时集合
backTracking(res, list, nums, i + 1); // 第四步 回溯
list.remove(list.size() - 1); // 第五步 元素从临时集合移除
这三句的意思是:
假设结果需要cand [i],然后继续使用另一个调用backTracking, 然后我么你不需要cand [i]了,只有两种情况,要或者不要,可以尝试。
public class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
backTracking(res, new ArrayList<>(), nums, 0);
return res;
}
private void backTracking(List<List<Integer>> res, List<Integer> list, int[] nums, int start){
// 第0步,加入停止条件
res.add(new ArrayList<>(list)); // 第一步 满足条件的临时集合加入结果集
for(int i = start; i < nums.length; ++i){ // 第二步for循环 遍历所有的元素
list.add(nums[i]); // 第三步 元素加入临时集合
backTracking(res, list, nums, i + 1); // 第四步 回溯
list.remove(list.size() - 1); // 第五步 元素从临时集合移除
}
}
}
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
res = []
state = []
self.backtracking(state, nums, res, 0)
return res
def backtracking(self, state, nums, res, pos):
# 1 是否满足条件
if True:
# 2 加入结果
res.append(state.copy())
# 返回return,可选
# 4 for 遍历
for i in range(pos, len(nums)):
# 5 满足额外条件
if True:
state.append(nums[i])
self.backtracking(state, nums, res, i+1)
state.pop()
当需要搜索的是数组子序列、不包含重复元素的子集或是有其他限制条件的问题,我们通常需要使用pos参数。pos参数可以帮助我们限制在回溯过程中搜索的起始位置,从而避免重复。例如,在求解子集问题时,我们用pos来确保只对后面的元素进行选择,从而避免出现重复的子集
90. Subsets II
返回数组(里面有重复的数字)的不含重复结果的子集
public class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
backTracking(res, new ArrayList<>(), nums, 0);
return res;
}
private void backTracking(List<List<Integer>> res, List<Integer> list, int[] nums, int start){
if(!res.contains(list)) res.add(new ArrayList<>(list)); //检查是否包含结果,不包含才加入
for(int i = start; i < nums.length; ++i){
list.add(nums[i]);
backTracking(res, list, nums, i + 1);
list.remove(list.size() - 1);
}
}
}
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort()
res = []
state = []
checked = set() # 在向res添加state前进行判断,来避免添加重复的子集,那么我们需要一种能有效地对两个列表进行比较的方法。但是,对于Python的列表来说,当使用in运算符进行比较时是基于引用进行判断的,即检查的是他们在内存中是否占据同一位置,而不是它们是否包含相同的内容。因此,我们需要先将state转换为一个可以被哈希的类型(例如元组),然后将其添加到一个集合中进行快速的查找。
self.backtracking(nums, res, state, checked, 0)
return res
def backtracking(self, nums, res, state, checked, pos):
state_tuple = tuple(state)
if state_tuple not in checked:
res.append(state[:])
checked.add(state_tuple)
for i in range(pos, len(nums)):
if True:
state.append(nums[i])
self.backtracking(nums, res, state, checked, i+1)
state.pop()
如果没有现排序的话,就会出现
Wrong Answer
15 / 20 testcases passed
Editorial
Input
nums =
[4,4,4,1,4]
Use Testcase
Output
[[],[4],[4,4],[4,4,4],[4,4,4,1],[4,4,4,1,4],[4,4,4,4],[4,4,1],[4,4,1,4],[4,1],[4,1,4],[1],[1,4]]
Expected
[[],[1],[1,4],[1,4,4],[1,4,4,4],[1,4,4,4,4],[4],[4,4],[4,4,4],[4,4,4,4]]
46. Permutations 全排列 注意(全排列的每一种可能都是需要从nums的第一个元素开始遍历的)
给你一个数字不重复的数组,求这个数组的所有排列
时间复杂度O(n!)
public class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
backTracking(res, new ArrayList<>(), nums);
return res;
}
private void backTracking(List<List<Integer>> res, List<Integer> list, int[] nums){
if(list.size() == nums.length) res.add(new ArrayList<>(list));
for(int i = 0; i < nums.length; ++i){
if(list.contains(nums[i])) continue; // 跳过重复的,不然会出现[nums[0], nums[0], nums[0]] 这种情况
list.add(nums[i]);
backTracking(res, list, nums);
list.remove(list.size() - 1);
}
}
}
当需要搜索的是数组或其他序列的全部组合或排列时,我们通常不需要pos参数。此时对序列的每一个元素,我们都需要进行选择和不选择两种操作。例如,在求解字符串的全排列问题时,我们需要遍历序列的每一个元素。
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res = []
self.bc(res, [], nums)
return res
def bc(self, res, path, nums):
if len(path) == len(nums):
res.append(path[:])
return
for i in nums:
if i in path:
continue
path.append(i)
self.bc(res, path, nums)
path.pop()
排列是需要把所有的元素都遍历一遍,所以在for循环中,我们需要从0开始,而不是从给定的start开始。同时,要在遍历的过程中去除已经使用过的元素,所以在添加路径前,我们需要判断这个元素是否已经在路径中,如果已经存在则跳过。
77.
Combinations 组合
77. Combinations
给你两个整数n和k,返回在1到n个数里面所有可能的k个数字的组合。
public class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new ArrayList<>();
backTracking(res, new ArrayList<>(), n, k, 1);
return res;
}
private void backTracking(List<List<Integer>> res, List<Integer> list, int n, int k, int start){
if(list.size() == k) res.add(new ArrayList(list));
for(int i = start; i <= n; ++i){
list.add(i);
backTracking(res, list, n, k, i + 1);
list.remove(list.size() - 1);
}
}
}
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
res = []
self.bc(res, [], n, k, 1)
return res
def bc(self, res, path, n, k, start):
if len(path) == k:
res.append(path[:])
return
for i in range(start, n+1):
path.append(i)
self.bc(res, path, n, k, i+1)
path.pop()
39. Combination Sum 组合总和 着重看看这个 (3剪枝的具体位置,和需要start(因为结果不能重复))
给定一个数组candidates,和一个整数target,返回所有candidates里面元素的组合的和等于target。对candidates里面的元素出现的次数无限制。
例如:
candidate : [2, 3, 6, 7] | target: 7
返回:
public class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(candidates);
backTracking(res, new ArrayList<>(), candidates, target, 0);
return res;
}
private void backTracking(List<List<Integer>> res, List<Integer> list, int[] candidate, int target, int start){
if(target < 0) return;
if(target == 0) res.add(new ArrayList<>(list));
else {
for(int i = start; i < candidate.length; ++i){
list.add(candidate[i]);
backTracking(res, list, candidate, target - candidate[i], i);
list.remove(list.size() - 1);
}
}
}
}
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
res = []
self.backtracking(res, [], candidates, target, 0)
return res
def backtracking(self, res, path, candidates, target, start):
# 1 判断条件
if target == 0:
res.append(path[:])
return
# 2 循环
for i in range(start, len(candidates)):
# 3 剪枝
if target-candidates[i] < 0:
break
target -= candidates[i]
path.append(candidates[i])
self.backtracking(res, path, candidates, target, i)
target += candidates[i]
path.pop()
self.backtracking(res, path, candidates, target, i+1)的最后一个参数应该传i而不是i+1。这是因为这个问题允许每个数字在组合中出现多次,如果把i+1传给start,那么下次循环将不再考虑当前的数字
40. Combination Sum II 组合总和 II 因为元素只能取一次,所以排序,i+1和跳过相等元素
给定一个数组candidates,和一个整数target,返回所有candidates里面元素的组合的和等于target。对candidates里面的元素最多只能出现一次。
意思是数组里面的数字只能用一次,而不是不能用里面重复的数字。
例如:
candidate : [10, 1, 2, 7, 6, 1, 5]
target: 8
返回:
if(i > start && candidate[i] == candidate[i - 1]) continue;
这句是用来跳过重复的数字的。当然也可以通过在将符合条件的候选数组加入结果的时候判断是否已经含有重复的,但是这样很慢。
public class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(candidates);
backTracking(res, new ArrayList<>(), candidates, target, 0);
return res;
}
private void backTracking(List<List<Integer>> res, List<Integer> list, int[] candidate, int target, int start){
if(target < 0) return;
if(target == 0) res.add(new ArrayList<>(list));
else {
for(int i = start; i < candidate.length; ++i){
if(i > start && candidate[i] == candidate[i - 1]) continue;
list.add(candidate[i]);
backTracking(res, list, candidate, target - candidate[i], i + 1);
list.remove(list.size() - 1);
}
}
}
}
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
res = []
self.backtracking(res, [], candidates, target, 0)
return res
def backtracking(self, res, path, candidates, target, start):
# 1 判断条件
if target == 0:
res.append(path[:])
return
# 2 循环
for i in range(start, len(candidates)):
# 3 剪枝
if target-candidates[i] < 0:
break
if i > start and candidates[i] == candidates[i-1]:
continue
target -= candidates[i]
path.append(candidates[i])
self.backtracking(res, path, candidates, target, i+1)
target += candidates[i]
path.pop()
216. Combination Sum III
找到所有可能的k个数字组合,它们加起来为n,假设只能使用1到9的数字,并且每个组合应该是一组唯一的数字。
public class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
List<List<Integer>> res = new ArrayList<>();
backTracking(res, new ArrayList<>(), k, n, 1);
return res;
}
private void backTracking(List<List<Integer>> res, List<Integer> list, int k, int n, int start){
if(n < 0) return;
if(n == 0 && list.size() == k) res.add(new ArrayList<>(list));
for(int i = start; i <= 9; i++){
list.add(i);
backTracking(res, list, k, n-i, i+1);
list.remove(list.size()-1);
}
}
}
784. Letter Case Permutation
给定一个字符串s,我们可以将这个字符串里面的每一个字符转换成大写或者小写,返回所有我们可能生成的字符。
拿s=abc举例,backtracking如下图所示:
class Solution {
public List<String> letterCasePermutation(String s) {
if(s == null || s == "") return new ArrayList<String>();
List<String> res = new ArrayList<String>();
backTracking(s, res, 0);
return res;
}
public void backTracking(String s, List<String> res, int i){
// 第0步:停止条件
// 第1步:满足条件的临时集合加入结果集
if(i == s.length()){
res.add(s);
return;
}
if(s.charAt(i) >= '0' && s.charAt(i) <= '9'){
backTracking(s, res, i+1);
return; // 下面的就不要执行了
}
// 下面两个相当于for循环
char[] cs = s.toCharArray();
cs[i] = Character.toLowerCase(cs[i]);
backTracking(String.valueOf(cs), res, i+1); // 继续回溯
cs[i] = Character.toUpperCase(cs[i]);
backTracking(String.valueOf(cs), res, i+1);
}
}
faster than 79.59% of Java online submissions for Letter Case Permutation.
17. Letter Combinations of a Phone Number
17. Letter Combinations of a Phone Number
手机键盘拨号,给你一串数字,问对应手机九宫格的输入法有可能输出那些字符串。
手机键盘:
输入:
“23”
输出:
[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”]
public class Solution {
private String[] letter = new String[] {" ", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) {
List<String> res = new ArrayList<>();
if(digits == null || digits.length() == 0) return res;
char[] nums = digits.toCharArray();
backTracking(res, new StringBuilder(), nums, 0);
return res;
}
private void backTracking(List<String> res, StringBuilder build, char[] nums, int start){
if(build.length() == nums.length) res.add(build.toString());
for(int i = start; i < nums.length; ++i){
int index = Integer.parseInt(String.valueOf(nums[i]));
for(int j = 0; j < letter[index].length(); ++j){
build.append(letter[index].charAt(j));
backTracking(res, build, nums, i + 1);
build.deleteCharAt(build.length() - 1);
}
}
}
}
22. Generate Parentheses 括号生成器
22. Generate Parentheses 括号生成器
public class Solution {
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
backTracking(res, new StringBuilder(), 0, 0, n);
return res;
}
private void backTracking(List<String> res, StringBuilder builder, int left, int right, int n){
if(builder.length() == n*2) res.add(builder.toString());
else {
if(left < n){
builder.append("(");
backTracking(res, builder, left+1, right, n);
builder.deleteCharAt(builder.length()-1);
}
if(right < n && left > right){
builder.append(")");
backTracking(res, builder, left, right+1, n);
builder.deleteCharAt(builder.length()-1);
}
}
}
}
131. Palindrome Partitioning 分割回文字串
131. Palindrome Partitioning 分割回文字串
给你一个字符串s,分割字符串s使得每个子字符串都是回文,返回所有的分割结果。
如s = “aab”
返回:
[
[“aa”,“b”],
[“a”,“a”,“b”]
]
public class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
backTracking(res, new ArrayList<>(), s, 0);
return res;
}
private void backTracking(List<List<String>> res, List<String> list, String s, int start){
if(start == s.length()) res.add(new ArrayList<>(list)); //start == s.length()的时候,说明已经带s的最后一个字符了
else {
for(int i = start; i < s.length(); i++){
if(isPalindrome(s, start, i)){
list.add(s.substring(start, i+1));
backTracking(res, list, s, i+1);
list.remove(list.size() - 1);
}
}
}
}
/*
* 判断是否是回文
*/
private boolean isPalindrome(String s, int low, int high){
while(low < high){
if(s.charAt(low++) != s.charAt(high--)) return false;
}
return true;
}
}
class Solution:
def partition(self, s: str) -> List[List[str]]:
res = []
path = []
self.backtrackin(path, res, s, 0)
return res
def backtrackin(self, path, res, s, start):
if start == len(s):
res.append(path[:])
return
for i in range(start, len(s)):
if self.is_p(s, start, i):
path.append(s[start:i+1])
self.backtrackin(path, res, s, i+1)
path.pop()
def is_p(self, s, l, r):
new_s = s[l:r+1]
return new_s == new_s[::-1]
52. N-Queens II N皇后
返回n皇后有多少种摆法。
n皇后问题是指在
n
∗
n
n*n
n∗n的棋盘上要摆n个皇后,要求任何皇后不同行,不同列,不在一条直线上。
有了 Palindrome Partitioning 分割回文字串 的基础,我们就可以解决经典的 N-皇后 的问题了:
用一个数组queen保存已经摆放皇后的位置,queen[i]的值表示第i行皇后所在的列数。
class Solution {
int count = 0;
public int totalNQueens(int n) {
if(n < 1) return 0;
int[] queen = new int[n];
backTracking(queen, n, 0);
return count;
}
public void backTracking(int[] queen, int n, int start){
if(start == n){
count++;
return;
}
for(int j=0; j < n; j++){
if(check(queen, start, j)){
queen[start] = j;
backTracking(queen, n, start+1);
}
}
}
public boolean check(int[] queen, int row, int col){
for(int i = 0; i < row; i++){
int queenRow = queen[i];
if(queenRow == col) return false;
if(Math.abs(queenRow-col) == Math.abs(i-row)) return false;
}
return true;
}
}
faster than 52.25% of Java online submissions for N-Queens II.
51. N-Queens n皇后
设计一种算法,打印n皇后在n*n棋盘上的各种摆法,其中每个皇后都不同行,不同列,也不在对角线上。
public class Solution {
public List<List<String>> solveNQueens(int n) {
List<Integer[]> res = new ArrayList<>();
backTracking(res, new Integer[n], n, 0);
//以下只是把皇后放置在棋盘上
List<List<String>> resList = new ArrayList<>();
for(Integer[] nums: res){
List<String> list = new ArrayList<>();
for(int i = 0; i < nums.length; i++){
StringBuilder sb = new StringBuilder();
for(int j = 0; j < n; j++){
if(j == nums[i]) sb.append("Q");
else sb.append(".");
}
list.add(sb.toString());
}
resList.add(list);
}
return resList;
}
/*
* queen[] 是表示:下表i代表第i行的第queen[i]列放置一个皇后
*/
private void backTracking(List<Integer[]> res, Integer[] queen, int n, int start){
if(start == n) res.add(queen.clone());
else {
for(int i = 0; i < n; i++){ //不是从start开始
if(checkValid(queen, start, i)){
queen[start] = i;
backTracking(res, queen, n, start+1); //不是i+1,i只是和列有关
}
}
}
}
/*
* 检查在位置(row1,col1)是否可以放置皇后
*/
private boolean checkValid(Integer[] queen, int row1, int col1){
for(int row2 = 0; row2 < row1; row2++){
int col2 = queen[row2];
if(col2 == col1) return false; //两个皇后放置在同一列上
int colSize = Math.abs(col1 - col2);
int rowSize = row1 - row2;
if(colSize == rowSize) return false; //两个皇后放置在同一对角线上
}
return true;
}
}
526. Beautiful Arrangement 漂亮排列
526. Beautiful Arrangement 漂亮排列
假设你有从1到N的N个整数。如果在这个数组中第i个位置(1 <= i <= N)的下列之一为真,我们将这个N个数字成功构造的数组定义为一个漂亮的排列:
- 第i个位置的数字可以被i整除。
- i可以被第i个位置的数字整除。
现在给出N,你可以建造多少漂亮排列?
一开始的时候我是穷举所有的排列,再去看这些排列是否是漂亮排列,结果超时了,因为时间复杂度是
O
(
n
!
)
O(n!)
O(n!)
Time Limit Exceeded
超时的代码:
class Solution {
public int countArrangement(int n) {
List<List<Integer>> res = new ArrayList<>();
backTracking(res, new ArrayList<>(), n);
return res.size();
}
public void backTracking(List<List<Integer>> res, List<Integer> temp, int n){
if(temp.size() == n){
if(isBeautiful(temp)){
res.add(new ArrayList<>(temp));
}
return;
}
for(int i = 1; i <= n; i++){
if(temp.contains(i)) continue;
temp.add(i);
backTracking(res, temp, n);
temp.remove(temp.size()-1);
}
}
public boolean isBeautiful(List<Integer> list){
for(int i = 1; i <= list.size(); i++){
if(i%list.get(i-1) != 0 && list.get(i-1)%i != 0) return false;
}
return true;
}
}
然后稍微修改一下,在把每一个元素加进排列的时候就去检查时候满足漂亮排列的要求,这样就能减少时间了:
class Solution {
public int countArrangement(int n) {
List<List<Integer>> res = new ArrayList<>();
int[] nums = new int[n];
for(int i = 1; i <= n; i++){
nums[i-1] = i;
}
backTracking(res, new ArrayList<>(), nums, 1);
return res.size();
}
public void backTracking(List<List<Integer>> res, List<Integer> temp, int[] nums, int index){
if(temp.size() == nums.length){
res.add(new ArrayList<>(temp));
return;
}
for(int i = 0; i < nums.length; i++){
if(temp.contains(nums[i])) continue;
if(index%nums[i] == 0 || nums[i]%index == 0){
temp.add(nums[i]);
backTracking(res, temp, nums, index+1);
temp.remove(temp.size()-1);
}
}
}
}
faster than 2.82% of Java online submissions for Beautiful Arrangement.不过这也太慢了一点。后面还要看看其他方法。
401. Binary Watch
二进制手表顶部有4个LED,代表小时(0-11),底部的6个LED代表分钟(0-59)。给定非负整数n表示当前亮的LED数量,返回手表可能代表的所有可能时间。如:
Input: n = 1
Return: [“1:00”, “2:00”, “4:00”, “8:00”, “0:01”, “0:02”, “0:04”, “0:08”, “0:16”, “0:32”]
这道题用上组合和排列的思想可以解决。不知道为什么是easy…可能是有更简单的方法。
因为一共有n个LED灯亮了,所以,如果分给时钟的灯有k个的话,那么分给分钟的灯就有n-k个。那么用组合的思想,求出:
- k个时钟的灯能够组成多少种时钟的数值?
- n-k个分钟的灯够组成多少种分钟的数值?
在用排列的思想将上面的结果进行排列,便可求得结果。
public class Solution {
public List<String> readBinaryWatch(int num) {
List<String> res = new ArrayList<>();
int[] h = new int[]{8,4,2,1};
int[] m = new int[]{32,16,8,4,2,1};
for(int i = 0; i <= num; i++){
List<Integer> hList = getDigit(h, i);
List<Integer> mList = getDigit(m, num-i);
for(int n1 : hList){
if(n1 >= 12) continue;
for(int n2 : mList){
if(n2 >= 60) continue;
//res.add(n1 + ":" + (n2 < 10 ? "0" + n2 : n2));
res.add(String.format("%d:%02d", n1, n2));
}
}
}
return res;
}
public List<Integer> getDigit(int[] nums, int count){
List<Integer> res = new ArrayList<>();
helper(nums, res, count, 0, 0);
return res;
}
public void helper(int[] nums, List<Integer> res, int count, int pos, int sum){
if(count == 0){
res.add(sum);
return;
}
for(int i = pos; i < nums.length; i++){
helper(nums, res, count-1, i+1, sum + nums[i]);
}
}
}
faster than 19.11% of Java online submissions for Binary Watch.