参考:https://labuladong.gitbook.io/algo/di-ling-zhang-bi-du-xi-lie/hui-su-suan-fa-xiang-jie-xiu-ding-ban
解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
排列需要用record来记住,因为避免重复取了之前取过的;组合虽然也需要,用处不同,组合不是为了避免重复取了之前取过的,不可能取到之前的。因为其通过sign已经记录了,他的作用是避免重复的相等的数,虽然在选择列表里的不同位置,但是也被取到。 if i >= 1 and nums[i-1] == nums[i] and i-1 not in record: continue
遇到全排列\组合直接想回溯框架。不要纠结DFS/BFS
遇到重复的列表情况,再不济就先用列表,然后转tuple,放进set集合,就可以排除重复情况。如果是列表,可以先sorted
回溯算法框架
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,特别简单。
回溯本就是暴力!!!
全排列的代码
List<List<Integer>> res = new LinkedList<>();
/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
// 记录「路径」
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums, track);
return res;
}
// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
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 (track.contains(nums[i]))
continue;
// 做选择
track.add(nums[i]);
// 进入下一层决策树
backtrack(nums, track);
// 取消选择
track.removeLast();
}
}
47. 全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
切记这里函数里面如果不传参很容易出错~~~因为track一直在变化。不然就要list(track)///或者拷贝
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
if not nums:
return []
out = []
check = {}
nums = sorted(nums)
for i in range(len(nums)):
check[i] = 0
def backtrack(nums, track, check): #巧用了check 其实就是DFS
if len(track) == len(nums):
out.append(track)
return
for i in range(len(nums)):
if check[i] == 1:
continue
if i >= 1 and nums[i] == nums[i-1] and check[i-1] == 0:
continue # 如果前一个是相同的,则跳过!!!!
check[i] = 1
backtrack(nums, track + [nums[i]], check)
check[i] = 0
backtrack(nums, [], check)
return out
77. 组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
这个题 容易忽略的一个点是 里面的回溯 使用的是i,不是sign+1,这个要切记!!!理清含义。
注意剪枝的作用!!
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
nlist = []
check = {}
for i in range(1, n+1):
nlist.append(i)
check[i-1] = 0
if n == k:
return [nlist]
out = []
def backtrack(check, track, sign):
if len(track) == k:
out.append(track)
return
for i in range(len(nlist)):
if i < sign:
continue
if check[i] == 1:
continue
if len(track) + n - i == k: # 好强悍的剪枝
out.append(track+nlist[i:])
return
elif len(track) + n - (i + 1) < k:
return
check[i] = 1
# track.append(nlist[i]) 不可这么写,直接改变了!后面就还需要pop!
backtrack(check, track+[nlist[i]], i)
check[i] = 0
backtrack(check, [], 0)
return out
无重复元素+无限制重复–此时就是直接用了一个标记,下一次不能小于上一次的标记,也就是不会返回。只会重复当前数或者是往后走~~ 39组合总和
如果只是无重复元素,每一个只能使用一次的话,那么直接用字典记录就可以了。
有重复元素, 只能使用一次-- 先排序,多使用一个字典来标记,记住全部标记序号,如果nums[i] == nums[i-1] and i-1 not in record,则continue;
有重复元素,无限次使用,其实没有意义,重复包含在无限次里了。
39. 组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
有重复情况下, 没时间只能用以下的set()
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
if not candidates:
return []
out = set()
candidates = sorted(candidates)
def backtrack(track, track_sum):
if track_sum == target:
out.add(tuple(sorted(track)))
return
if track_sum > target:
return
for i in range(len(candidates)):
backtrack(track+[candidates[i]], track_sum+candidates[i])
backtrack([], 0)
return list(out)
优化重复情况:
用一个序号来记录,每次都只要大于等于记号的,就能保证能在同一个数不断地取,也不会返回之前的数。
class Solution: # 优化了重复
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
if not candidates:
return []
out = []
candidates = sorted(candidates)
def backtrack(track, track_sum, sign):
if track_sum == target:
out.append(track)
return
if track_sum > target:
return
for i in range(len(candidates)):
if i < sign:
continue
backtrack(track+[candidates[i]], track_sum+candidates[i], i)
backtrack([], 0, 0)
return out
40. 组合总和 II
与上一题的主要区别是包含重复数字,所以在解决不重复抽取同一个数组中的数并无法避免因为数组中有重复的数字而导致的重复的组合的问题。
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
下面是用了check来剪掉上述所说的重复情况。
# 注意,即使避免了在candidates中抽取重复的数,但是也有可能其之中有相同的数,所以还是会产生重复的组合。
class Solution:
def combinationSum2(self, candidates, target):
if not candidates:
return []
candidates = sorted(candidates)
out = []
check = {}
for i in range(len(candidates)):
check[i] = 0
def backtrack(track, sum_track, sign):
if sum_track == target:
out.append(track)
return
elif sum_track > target:
return
for i in range(len(candidates)):
if i < sign:
continue
if i > 0 and candidates[i] == candidates[i-1] and check[i-1] == 0:
continue
check[i] = 1
backtrack(track+[candidates[i]], sum_track+candidates[i], i+1)
check[i] = 0
backtrack([], 0, 0)
return out
老套路了,有重复数字则用check来判断,用sign来避免重复抽取
90. 子集 II----使用check注意排序
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
if not nums:
return [[]]
out = []
check = {}
nums = sorted(nums) # 注意排序!!!
for i in range(len(nums)):
check[i] = 0
def backtrack(track, sign):
out.append(track)
for i in range(len(nums)):
if i < sign:
continue
if i > 0 and nums[i] == nums[i-1] and check[i-1] == 0: #说明第二个重复的数开始当家作主
continue
check[i] = 1
backtrack(track+[nums[i]], i+1)
check[i] = 0
backtrack([], 0)
return out
集合的所有子集
# @param A int整型一维数组
# @return int整型二维数组
#
class Solution:
def subsets(self, A):
if not A:
return [[]]
A = sorted(A)
result = [[]]
for i in range(len(A)):
res = []
for j in range(len(result)):
res.append(result[j]+[A[i]])
result += res
return result
class Solution:
def subsets(self, A):
if not A:
return [[]]
A = sorted(A)
result = []
def backtrack(sub, sign):
result.append(sub)
for i in range(sign, len(A)):
backtrack(sub+[A[i]], i+1)
backtrack([], 0)
return result
剑指offer200页
##立方体,三组面的顶点分别相等。
# a1+a2+a3+a4 = a5+a6+a7+a8; a1+a3+a5+a7 = a2+a4+a6+a8; a1+a2+a5+a6=a3+a4+a7+a8
def find_track(nums):##这是排列题,考虑重复
result = []
nums = sorted(nums)
record = {}
def backtrack(track):
if len(track) == len(nums):
if (track[0]+track[1]+track[2]+track[3]) == (track[4]+track[5]+track[6]+track[7]) and (track[0]+track[2]+track[4]+track[6]) == (track[1]+track[3]+track[5]+track[7]) and (track[0]+track[1]+track[4]+track[5]) == (track[2]+track[3]+track[6]+track[7]):
result.append(track)
return
for i in range(len(nums)):
if i >= 1 and nums[i] == nums[i-1] and i-1 not in record:
continue
if i in record:
continue
record[i] = 1
backtrack(track+[nums[i]])
record.pop(i)
backtrack([])
return result
print(find_track([1,1,1,1,1,1,1,1]))
其实排列组合问题,比较难的还是N皇后问题。
N皇后问题–easy
考虑三个方面:# 从上往下,同行不需要考虑;考虑同列-以及对角线:对角线的规律是y-x相同或x+y相同
import copy
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
# 从上往下,同行不需要考虑;考虑同列-以及对角线:对角线的规律是y-x相同或x+y相同
if n <= 0:
return []
record = {}
res1 = {}
res2 = {}
result = []
dp = []
for i in range(n):
dp.append(["."]*n)
def backtrack(n, row):
if row == n:
sub = []
for i in range(len(dp)):
sub.append("".join(dp[i]))
result.append(sub)
return
for i in range(n):
if i in record:
continue
if i + row in res2:
continue
if i - row in res1:
continue
record[i] = 1
res1[i-row] = 1
res2[i+row] = 1
dp[row][i] = "Q"
backtrack(n, row+1)
record.pop(i)
res1.pop(i-row)
res2.pop(i+row)
dp[row][i] = "."
backtrack(n, 0)
return result```