子集型回溯模版1
[1, 2] -> [ [], [1], [2], [1, 2] ]
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
ans = []
def dfs(i, path):
ans.append(path[:])
for j in range(i, n):
path.append(nums[j])
dfs(j+1, path)
path.pop()
dfs(0, [])
return ans
i为目前的位置, j为下一个考虑的位置. 这种情况就应该把每一步都加到ans里, 而不是等到i==n
假设存在dup, 则我们应该在横向去重(for loop里)
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort() # 首先对数组进行排序
n = len(nums)
ans = []
def dfs(i, path):
ans.append(path[:])
for j in range(i, n):
if j > i and nums[j] == nums[j-1]:
continue # 跳过重复元素
path.append(nums[j])
dfs(j+1, path)
path.pop()
dfs(0, [])
return ans
77 组合型回溯模版
n choose k
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
nums = list(range(1, n + 1))
ans = []
def dfs(i, path):
if len(path) == k:
ans.append(path[:])
return
for j in range(i, n):
path.append(nums[j])
dfs(j+1, path)
path.pop()
dfs(0, [])
return ans
只能说思路和子集一模一样, 加入ans的条件变了而已
排列(permutation)型
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
ans = []
n = len(nums)
def dfs(path):
if len(path) == n:
ans.append(path[:])
return
for i in range(n):
if nums[i] in path:
continue
path.append(nums[i])
dfs(path)
path.pop()
dfs([])
return ans
有重复元素的permutation
同样也是横向去重, 所以我们在for loop前创建一个set来表示已经被用过的元素
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
ans = []
n = len(nums)
nums.sort() # 排序以便于处理重复元素
def dfs(path, indices):
if len(path) == n:
ans.append(path[:])
return
used = set() # 用于跟踪在当前位置使用过的元素
for index in list(indices): # 转换为列表以便在循环中修改
if nums[index] in used:
continue # 跳过在当前位置已经使用过的重复元素
used.add(nums[index])
path.append(nums[index])
new_indices = indices.copy()
new_indices.remove(index)
dfs(path, new_indices)
path.pop()
initial_indices = set(range(n))
dfs([], initial_indices)
return ans
力扣知识点: 看是横向遍历还是纵向遍历
好题: 77, 39, 78, 46
i+1和start + 1的根本区别: 考不考虑当前元素重复加进去, 可以看40和78的区别
python知识点
result.append(path[:])和 result.append(path[:])
区别是前者拷贝, 后者引用
能解决的问题
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等
通用模版
void backtracking(当前参数已经选了哪些) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
77 组合 (经典中的经典)
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
result = [] # 存放结果集
self.backtracking(n, k, 1, [], result)
return result
def backtracking(self, n, k, startIndex, path, result):
if len(path) == k:
# 注意这个地方不能path, 得path[:], 相当于copy了一次
result.append(path[:])
return
for i in range(startIndex, n + 1): # 需要优化的地方
path.append(i) # 处理节点
self.backtracking(n, k, i + 1, path, result)
path.pop() # 回溯,撤销处理的节点
22 有效的括号组合
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
ans = []
def dfs(l, r, path):
if l == r:
if len(path) == 2*n:
ans.append("".join(path))
return
if r > l or l > n or r > n:
return
# add ')'
path.append(")")
dfs(l, r+1, path)
path.pop()
# add (
path.append("(")
dfs(l + 1, r, path)
path.pop()
dfs(0,0,[])
return ans
216 组合总和III
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9
每个数字 最多使用一次
class Solution:
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
res = []
def back(cur_ans, cur_num):
if len(cur_ans) == k and sum(cur_ans) == n:
res.append(cur_ans[:])
return
if len(cur_ans) >= k or cur_num > 9:
return
for i in range(cur_num, 10):
cur_ans.append(i)
back(cur_ans, i + 1)
cur_ans.pop()
back([], 1)
return res
唯一的不同就是放进去res的条件不同, 注意是两个哦
17 电话号码的字母组合
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if digits == "":
return []
res = []
def back(cur_answer, cur_index):
if cur_index == len(digits):
res.append(cur_answer)
return
for letter in self.letterMap[int(digits[cur_index])]:
cur_answer = cur_answer + letter
back(cur_answer, cur_index + 1)
back("", 0)
return res
一定一定注意不能向上面这样写, 因为状态持续性: 当您这样修改cur_answer时,这个变化在递归调用返回后仍然存在。这意味着在循环的下一次迭代中,您会从一个包含了上一次迭代字母的cur_answer开始。所以我们只能创建新变量
如果不创建新的话, 共用一个cur的话, 那么撤销这一步是必须的
39 组合总和
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
关键的不同之处就在于可以选择重复的数字了, 直觉上最明显的改变应该在从哪里开始遍历这个问题上
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
res = []
def back(cur):
if (sum(cur) > target):
return
if (sum(cur) == target):
res.append(cur[:])
for i in range(len(candidates)):
cur.append(candidates[i])
back(cur)
cur.pop()
back([])
return res
这样的话给出的结果是:
[[2,2,3],[2,3,2],[3,2,2],[7]]
问题就出在出现了重复项, 原因是考虑3的时候就不应该考虑2了. 证明我们的back停止条件还不完善
我比较鸡贼, 把前面改为了
加了一个条件:
if len(cur) >= 2 and cur[-1] < cur[-2]: return
如果倒数第一比倒数第二还要小, 那么证明我们往回看了
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
n = len(candidates)
candidates.sort()
res, temp = [], []
def dfs(i, cur_sum):
if cur_sum > target:
return
elif cur_sum == target:
res.append(temp[:])
else:
for j in range(i, n):
temp.append(candidates[j])
dfs(j, cur_sum + candidates[j])
temp.pop()
dfs(0, 0)
return res
学习一下标答
关键的一步就是在back中引入i来表明目前考虑了第几个index,
for j in range(i, n):
这一步就是从每个index开始尝试, 且index之前的东西都不考虑了
40 组合总和II, 上一题但无法选重复的, 每个元素只能用一次
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
res = []
def dfs(cur_ans, cur_index):
if sum(cur_ans) == target:
res.append(cur_ans[:])
return
if sum(cur_ans) > target:
return
for i in range(cur_index, len(candidates)):
cur_ans.append(candidates[i])
dfs(cur_ans, cur_index+1)
cur_ans.pop()
dfs([], 0)
return res
这样做会出现[1,1,1,1,1]的情况
精髓在于:回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
所以关键就是如何对同一层的数据进行去重
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
def dfs(begin, path, residue):
if residue == 0:
res.append(path[:])
return
for index in range(begin, size):
if candidates[index] > residue:
break
if index > begin and candidates[index - 1] == candidates[index]:
continue
path.append(candidates[index])
dfs(index + 1, path, residue - candidates[index])
path.pop()
size = len(candidates)
if size == 0:
return []
candidates.sort()
res = []
dfs(0, [], target)
return res
妙啊!因为题目要求每个元素只使用一次 当需要{1, 2, 2}的时候, 第二个2的 下标一定是 cur == begin; 当出现了一个{1,2,5}的时候,进行到第二次{1,2,5}到{1,2}的时候,第二个未完成的{1,2,5}的2的下标一定是cur>begin的,直接跳过就不会产生相同的{1,2,5}了,同理如果有第3个2也可以跳过!!!这个剪枝真的牛扳plus。。。兄弟作揖敬谢!
78 求所有子集
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
res = []
def dfs(cur, start):
res.append(cur[:])
for i in range(start, len(nums)):
cur.append(nums[i])
dfs(cur, i+1)
cur.pop()
dfs([], 0)
return res
还是那句话, 注意dfs那一步是从i+1还是start+1开始
131 求所有回文子串
class Solution:
def partition(self, s: str) -> List[List[str]]:
ans = []
n = len(s)
def dfs(i, path):
if i == n:
ans.append(path[:])
for j in range(i, n):
substring = s[i:j+1]
if substring == substring[::-1]:
path.append(substring)
dfs(j+1, path)
path.pop()
dfs(0, [])
return ans
90 求所有子集, 但原集合会有重复元素
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort()
res = []
def dfs(cur, start):
res.append(cur[:])
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i-1]:
continue
cur.append(nums[i])
dfs(cur, i + 1)
cur.pop()
dfs([], 0)
return res
舒服了直接秒杀, 就是上两道题的结合体
class Solution:
def subsetsWithDup(self, nums):
result = []
path = []
nums.sort() # 去重需要排序
self.backtracking(nums, 0, path, result)
return result
def backtracking(self, nums, startIndex, path, result):
result.append(path[:]) # 收集子集
uset = set()
for i in range(startIndex, len(nums)):
if nums[i] in uset:
continue
uset.add(nums[i])
path.append(nums[i])
self.backtracking(nums, i + 1, path, result)
path.pop()
这种用集合思想的方法也可以好好学习
记住for loop里的东西是同一层的, 只有递归那一步(dfs)会增加深度
这个地方你可以看到uset的创立后接的是for loop, 而这个set就是服务于这一层的, 所以最后不用返回之前的状态, 也不可以!!!
为什么不需要移除:
当我们从递归调用返回时,当前的 uset 就会被丢弃。
下一次循环会使用同一个 uset,但这正是我们想要的行为,因为我们想在同一层避免重复。
46 全排列
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res = []
def dfs(cur, indices_left):
if len(cur) == len(nums):
res.append(cur[:])
return
for i in indices_left:
cur.append(nums[i])
indices_left.remove(i)
dfs(cur, indices_left)
cur.pop()
indices_left.append(i)
dfs([], list(range(len(nums))))
return res
我一开始的代码是这样的, 忘记了一个关键问题, indices_left对于每一个cur是独一无二的, 所以我们每一次都得复制一遍new_indices_left = indices_left[:] new_indices_left.remove(i)
47 全排列II
全排列1但是有重复
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
res = []
nums.sort() # Sort to group duplicates together
def dfs(cur, indices_left):
if len(cur) == len(nums):
res.append(cur[:])
return
prev = None
for i in range(len(indices_left)):
if nums[indices_left[i]] == prev:
continue
new_indices = indices_left[:]
new_indices.remove(indices_left[i])
cur.append(nums[indices_left[i]])
dfs(cur, new_indices)
cur.pop()
prev = nums[indices_left[i]]
dfs([], list(range(len(nums))))
return res
这里给我们揭示了另外一种同层去重的方法, keep track of
prev
79 单词搜索
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
m = len(board)
n = len(board[0])
visited = [[False for _ in range(n)] for _ in range(m)]
def dfs(i, j, k):
if k == len(word):
return True
if i < 0 or j < 0 or i >= m or j >= n or visited[i][j] or board[i][j] != word[k]:
return False
visited[i][j] = True
res = (dfs(i-1, j, k+1) or
dfs(i+1, j, k+1) or
dfs(i, j-1, k+1) or
dfs(i, j+1, k+1))
visited[i][j] = False
return res
for i in range(m):
for j in range(n):
if dfs(i, j, 0):
return True
return False
51 N皇后
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
vertical = set()
diag1 = set()
diag2 = set()
res = []
def dfs(cur_row, path):
if len(path) == n:
res.append(path[:])
return
for pos in range(n):
if pos in vertical:
continue
if pos + cur_row in diag1 or pos - cur_row in diag2:
continue
vertical.add(pos)
diag1.add(pos + cur_row)
diag2.add(pos - cur_row)
path.append(pos)
dfs(cur_row + 1, path)
path.pop()
vertical.remove(pos)
diag1.remove(pos + cur_row)
diag2.remove(pos - cur_row)
dfs(0, [])
ans = []
for l in res:
temp = [["." for _ in range(n)] for _ in range(n)]
for i in range(n):
temp[i][l[i]] = 'Q'
temp[i] = "".join(temp[i])
ans.append(temp)
return ans
虽然写的有点丑, 但是能过就行了
37 解数独
class Solution:
def solveSudoku(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
self.solve(board)
def solve(self, board):
for i in range(9):
for j in range(9):
if board[i][j] == '.':
for num in '123456789':
if self.is_valid(board, i, j, num):
board[i][j] = num
if self.solve(board):
return True
board[i][j] = '.'
return False
return True
def is_valid(self, board, row, col, num):
# Check row
for x in range(9):
if board[row][x] == num:
return False
# Check column
for x in range(9):
if board[x][col] == num:
return False
# Check 3x3 box
start_row = (row // 3) * 3
start_col = (col // 3) * 3
for i in range(3):
for j in range(3):
if board[i + start_row][j + start_col] == num:
return False
return True