1 模板
解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:
- 路径:也就是已经做出的选择。
- 选择列表:也就是你当前可以做的选择。
- 结束条件:也就是到达决策树底层,无法再做选择的条件。
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
时间复杂度:O(N×N!)
2 例题
2.1 N皇后
public int queen(int n){
if (n < 1){
return 0;
}
int[] record = new int[n];
return process(0,record,n);
}
public int process(int i, int[] record, int n) {
if (i == n){
return 1;
}
int res= 0;
for (int j = 0; j < n; j++) {
if (isValiad(i,j,record)){
record[i] = j;
res += process(i +1, record, n);
}
}
return res;
}
public boolean isValiad(int i , int j , int[] record){
for (int k = 0; k < i; k++) {
if (record[k] == j || Math.abs(record[k] - j) == Math.abs(i - k)){
return false;
}
}
return true;
}
record的记录方式简化了撤销选择这一步。
2.2 全排列
2.2.1 LeetCode 46. 全排列
题目描述:
给定一个没有重复数字的序列,返回其所有可能的全排列。
用例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
参考代码:
方案一:
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> resList = new LinkedList<>();
LinkedList<Integer> trace = new LinkedList();
int[] visited = new int[nums.length];
backtrack(0, nums, trace, visited, resList);
return resList;
}
public void backtrack(int i, int[] nums, LinkedList<Integer> trace, int[] visited, List<List<Integer>> resList) {
if (i == nums.length) {
resList.add(new LinkedList<>(trace));
}
for (int j = 0; j < nums.length; j++) {
if (visited[j] == 0){
trace.addLast(nums[j]);
visited[j] = 1;
backtrack(i + 1, nums, trace, visited, resList);
trace.removeLast();
visited[j] = 0;
}
}
}
方案二:
直接交换。
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> resList = new LinkedList<>();
List<Integer> output = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
output.add(nums[i]);
}
backtrack(0, nums.length, output, resList);
return resList;
}
public static void backtrack(int i, int n, List<Integer> output, List<List<Integer>> resList) {
if (i == n) {
resList.add(new ArrayList(output));
return;
}
for (int j = i; j < n; j++) {
Collections.swap(output, i, j);
backtrack(i + 1, n, output, resList);
Collections.swap(output, i, j);
}
}
2.2.2 LeetCode 47. 全排列 II
题目描述:
给定一个可包含重复数字的序列,返回所有不重复的全排列。
用例:
输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
实现思路:
先排序,然后搜索过程中剪枝。
参考代码:
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> resList = new LinkedList<>();
LinkedList<Integer> trace = new LinkedList();
Arrays.sort(nums);
int[] visited = new int[nums.length];
backtrack(0, nums, trace, visited, resList);
return resList;
}
public void backtrack(int i, int[] nums, LinkedList<Integer> trace, int[] visited, List<List<Integer>> resList) {
if (i == nums.length) {
resList.add(new LinkedList<>(trace));
return;
}
int pre = 0;
boolean flag = false;
for (int j = 0; j < nums.length; j++) {
if (flag && pre == nums[j]){
continue;
}
if (visited[j] == 0){
trace.addLast(nums[j]);
visited[j] = 1;
backtrack(i + 1,nums,trace,visited,resList);
trace.removeLast();
visited[j] = 0;
flag = true;
pre = nums[j];
}
}
}
2.2.3 全排列 III permuteSkip
题目描述:
给定一个数n,代表小于等于n的所有自然数。给出这些自然数的全排列,限制条件是相邻两个数之间的差值大于1。
用例:
输入: 4
输出:
[
[2,4,1,3],
[3,1,4,2]
]
参考代码:
public static List<List<Integer>> permuteSkip(int n) {
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = i + 1;
}
LinkedList<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> trace = new LinkedList<>();
int[] visited = new int[n];
process(0, n, nums, visited, trace, res);
return res;
}
public static void process(int i, int n, int[] nums, int[] visited, LinkedList<Integer> trace, LinkedList<List<Integer>> res) {
if (i == n) {
res.add(new LinkedList<>(trace));
return;
}
for (int j = 0; j < n; j++) {
if (visited[j] == 0 && (i == 0 || Math.abs(trace.getLast() - nums[j]) > 1)) {
trace.addLast(nums[j]);
visited[j] = 1;
process(i + 1, n, nums, visited, trace, res);
trace.removeLast();
visited[j] = 0;
}
}
}
2.3 子集问题
2.3.1 LeetCode 78. 子集
题目描述:
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
用例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
参考代码:
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
process(0,nums,res,new ArrayList<>());
return res;
}
public void process(int start,int[] nums,List<List<Integer>> res,List<Integer> list){
res.add(new ArrayList<Integer>(list));
for(int j = start;j < nums.length;j++){
list.add(nums[j]);
process(j + 1,nums,res,list);
list.remove(list.size() - 1);
}
}
2.3.2 LeetCode 90. 子集 II
题目描述:
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
用例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
参考代码:
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
process(0,nums,res,new ArrayList<>());
return res;
}
public void process(int start,int[] nums,List<List<Integer>> res,List<Integer> list){
res.add(new ArrayList<Integer>(list));
for(int j = start;j < nums.length;j++){
if (j > start && nums[j] == nums[j - 1]){
continue;
}
list.add(nums[j]);
process(j + 1,nums,res,list);
list.remove(list.size() - 1);
}
}
2.4 组合问题
2.4.1 LeetCode 77. 组合
题目描述:
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
用例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
参考代码:
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new ArrayList();
process(0,n,k,new ArrayList(),res);
return res;
}
public void process(int start,int n,int k,List<Integer> trace,List<List<Integer>> res){
if(trace.size() == k){
res.add(new ArrayList(trace));
return;
}
for(int i = start;i < n;i++){
trace.add(i + 1);
process(i + 1,n,k,trace,res);
trace.remove(trace.size() - 1);
}
}
复用模板。
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new ArrayList();
int[] nums = new int[n];
for(int i = 0;i < n;i++){
nums[i] = i + 1;
}
process(0,n,k,nums,new ArrayList(),res);
return res;
}
public void process(int start,int n,int k,int[] nums,List<Integer> trace,List<List<Integer>> res){
if(trace.size() == k){
res.add(new ArrayList(trace));
return;
}
for(int i = start;i < n;i++){
trace.add(nums[i]);
process(i + 1,n,k,nums,trace,res);
trace.remove(trace.size() - 1);
}
}
2.4.2 LeetCode 39. 组合总和
题目描述:
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
用例:
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
参考代码:
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList();
process(0,candidates.length,candidates,target,new ArrayList(),res);
return res;
}
public void process(int start,int n,int[] nums,int target,List<Integer> trace,List<List<Integer>> res){
if(target == 0){
res.add(new ArrayList(trace));
return;
}
for(int i = start;i < n;i++){
if(target - nums[i] >= 0){
trace.add(nums[i]);
process(i,n,nums,target - nums[i],trace,res);
trace.remove(trace.size() - 1);
}
}
}
2.4.3 LeetCode 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]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
参考代码:
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList();
Arrays.sort(candidates);
process(0,candidates.length,candidates,target,new ArrayList(),res);
return res;
}
public void process(int start,int n,int[] nums,int target,List<Integer> trace,List<List<Integer>> res){
if(target == 0){
res.add(new ArrayList(trace));
return;
}
for(int i = start;i < n;i++){
if(i > start && nums[i - 1] == nums[i]){
continue;
}
if(target - nums[i] >= 0){
trace.add(nums[i]);
process(i + 1,n,nums,target - nums[i],trace,res);
trace.remove(trace.size() - 1);
}
}
}
2.4.4 LeetCode 216. 组合总和 III
题目描述:
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数。
- 解集不能包含重复的组合。
用例:
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
参考代码:
public List<List<Integer>> combinationSum3(int k, int n) {
List<List<Integer>> res = new ArrayList();
process(1,k,n,new ArrayList(),res);
return res;
}
public void process(int start,int k,int target,List<Integer> trace,List<List<Integer>> res){
if(trace.size() == k || target == 0){
if(trace.size() == k && target == 0){
res.add(new ArrayList(trace));
}
return;
}
for(int i = start;i <= 9;i++){
if(target - i >= 0){
trace.add(i);
process(i + 1,k,target -i,trace,res);
trace.remove(trace.size() - 1);
}
}
}
2.4.5 LeetCode 377. 组合总和 Ⅳ
题目描述:
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
用例:
nums = [1, 2, 3]
target = 4
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
因此输出为 7。
实现思路:
回溯+记忆化(时间复杂度较大)
参考代码:
回溯+记忆化
public int combinationSum4(int[] nums, int target) {
Arrays.sort(nums);
HashMap<Integer,Integer> cache = new HashMap();
return process(target,nums,cache);
}
public int process(int target,int[] nums,HashMap<Integer,Integer> cache){
if(target == 0){
return 1;
}
int res = 0;
if(cache.containsKey(target)){
return cache.get(target);
}else{
for(int i = 0;i < nums.length;i++){
if(target - nums[i] >= 0){
res += process(target - nums[i],nums,cache);
}
}
cache.put(target,res);
}
return res;
}
2.5 LeetCode 37. 解数独
2.6 LeetCode 22. 括号生成
2.7 吃汉堡
题目描述:
小红吃n天汉堡,要求每天吃的汉堡数目不一样,而且要尽可能多吃鸡肉汉堡,且尽可能少吃牛肉汉堡。
每天鸡肉汉堡供应数目a[i],牛肉汉堡供应数目b[i]。求至少要吃多少牛肉汉堡?
用例:
输入:
n = 5;
a = [1, 2, 2, 2, 4]
b = [4, 3, 5, 2, 1]
输出:
8
参考代码:
public int minBeef(int n, int[] a, int[] b) {
return process(0, n, 0, new HashSet<>(), a, b);
}
public int process(int day, int n, int sum, HashSet<Integer> cache, int[] a, int[] b) {
if (day == n) {
return sum;
}
int minSum = -1;
if (!cache.contains(a[day])) {
cache.add(a[day]);
int res = process(day + 1, n, sum, cache, a, b);
minSum = res;
cache.remove(a[day]);
}
if (!cache.contains(b[day])) {
cache.add(b[day]);
int res = process(day + 1, n, sum + b[day], cache, a, b);
minSum = res == -1 ? minSum : minSum == -1 ? res : Math.min(minSum, res);
cache.remove(b[day]);
}
return minSum;
}