回溯法:我的理解:一条路走到底,走不通了再返回。
第 39 题排序是为了剪枝。
第 40 题排序是为了去掉重复。
LeetCode 第 39 题:组合总和
传送门: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] ]
思路:深度优先遍历,即一条道走到底,不撞南墙不回头。
细节:先排序,升序或者倒序都可以,区别在于后续“剪枝”的时候是 break
还是 continue
。
Python 代码:预处理的时候,将 candidates
升序排列
class Solution:
def combinationSum(self, candidates, target):
"""
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
length = len(candidates)
if length == 0:
return []
# 排序为了后续剪枝
candidates.sort()
res = []
self.__dfs(candidates, length, 0, [], target, res)
return res
def __dfs(self, candidates, length, start, path, residue, res):
"""
:param candidates: 无重复元素的数组
:param length: 数组的长度,避免一直 len(candidates)
:param start: 从 candidates 索引的第几位开始考虑
:param path: 深度优先搜索沿途经过的路径
:param residue: 剩余值
:param res: 存放结果集的列表
:return:
"""
if residue == 0:
res.append(path[:])
return
for index in range(start, length):
# 因为我们已经将数组预处理成升序数组,一旦发现当前遍历的数比剩余值还大,循环就可以停止了
if residue - candidates[index] < 0:
break
path.append(candidates[index])
# 注意:因为数字可以无限制重复被选取,因此起始位置还是自己
self.__dfs(candidates, length, index, path, residue - candidates[index], res)
path.pop()
Python 代码:预处理的时候,将 candidates
降序排列
class Solution:
def combinationSum(self, candidates, target):
"""
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
if len(candidates) == 0:
return []
candidates.sort(reverse=True)
res = []
self.__dfs(candidates, 0, target, [], res)
return res
def __dfs(self, candidates, start, residue, path, res):
# 先写递归终止条件
if residue == 0:
res.append(path[:])
return
for index in range(start, len(candidates)):
if residue - candidates[index] < 0:
continue
path.append(candidates[index])
self.__dfs(candidates, index, residue - candidates[index], path, res)
path.pop()
image-20190212072429407
最初的写法:没有排序,没有剪枝。
Java 代码:
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class Solution {
private List<List<Integer>> res = new ArrayList<>();
private int[] candidates;
private int len;
// residue 定义为剩余,这个剩余一开始等于 target,在递归中,它的值会越来越小
// 因为题目中说了"所有数字(包括 target)都是正整数"。
private void findCombinationSum(int residue, int start, Stack<Integer> pre) {
// 因为可以无限选取,所以 residue 只能小于 0 或者等于 0
if (residue < 0) {
return;
}
// 一定是剩下的那个数为 0 了,才表示我们所选的数字的和刚好等于 target
if (residue == 0) {
res.add(new ArrayList<>(pre));
return;
}
for (int i = start; i < len; i++) {
// 每个数有选择和不选择,因此尝试了一种解的可能以后要进行状态重置
pre.add(candidates[i]);
// 【关键】因为元素可以重复使用,这里递归传递下去的是 i 而不是 i + 1
findCombinationSum(residue - candidates[i], i, pre);
pre.pop();
}
}
public List<List<Integer>> combinationSum(int[] candidates, int target) {
int len = candidates.length;
if (len == 0) {
return res;
}
this.len = len;
this.candidates = candidates;
findCombinationSum(target, 0, new Stack<>());
return res;
}
public static void main(String[] args) {
int[] candidates = {2, 3, 6, 7};
int target = 7;
Solution solution = new Solution();
List<List<Integer>> combinationSum = solution.combinationSum(candidates, target);
System.out.println(combinationSum);
}
}
下面的写法有“剪枝”操作,首先先排了个序,请仔细体会。
Java 代码:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
// https://leetcode-cn.com/problems/combination-sum/description/
public class Solution2 {
private List<List<Integer>> res = new ArrayList<>();
private int[] candidates;
private int len;
private void findCombinationSum(int residue, int start, Stack<Integer> pre) {
if (residue == 0) {
res.add(new ArrayList<>(pre));
return;
}
// 优化添加的代码2:在循环的时候做判断,尽量避免系统栈的深度
// residue - candidates[i] 表示下一轮的剩余,如果下一轮的剩余都小于 0 ,就没有必要进行后面的循环了
// 这一点基于原始数组是排序数组的前提,因为如果计算后面的剩余,只会越来越小
for (int i = start; i < len && residue - candidates[i] >= 0; i++) {
pre.add(candidates[i]);
// 【关键】因为元素可以重复使用,这里递归传递下去的是 i 而不是 i + 1
findCombinationSum(residue - candidates[i], i, pre);
pre.pop();
}
}
public List<List<Integer>> combinationSum(int[] candidates, int target) {
int len = candidates.length;
if (len == 0) {
return res;
}
// 优化添加的代码1:先对数组排序,可以提前终止判断
Arrays.sort(candidates);
this.len = len;
this.candidates = candidates;
findCombinationSum(target, 0, new Stack<>());
return res;
}
public static void main(String[] args) {
int[] candidates = {2, 3, 6, 7};
int target = 7;
Solution2 solution = new Solution2();
List<List<Integer>> combinationSum = solution.combinationSum(candidates, target);
System.out.println(combinationSum);
}
}
参考资料:这里给出了把递归(借助“栈”)转成非递归的写法,其实就是模拟了一个栈,把参数都放到栈里。
LeetCode 第 40 题:组合总和 II
这道问题需要考虑提前停止搜索,即剪枝操作。
传送门: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] ]
思路:深度优先遍历。为了去重,首先得对 candidates
数组进行排序。
Python 代码:
class Solution:
def combinationSum2(self, candidates, target):
"""
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
length = len(candidates)
if length == 0:
return []
candidates.sort()
print(candidates)
res = []
self.__dfs(candidates, length, 0, [], target, res)
return res
def __dfs(self, candidates, length, start, path, residue, res):
if residue == 0:
res.append(path[:])
return
for index in range(start, length):
if candidates[index] > residue:
break
# 剪枝的前提是数组升序排序
if index > start and candidates[index - 1] == candidates[index]:
# [1, 1, 2, 5, 6, 7, 10]
# 0 号索引的 1 ,从后面 6 个元素中搜索
# 1 号索引也是 1 ,从后面 5 个元素(是上面 6 个元素的真子集)中搜索,这种情况显然上面已经包含
continue
path.append(candidates[index])
# 这里要传入 index + 1,因为当前元素不能被重复使用
# 如果 index + 1 越界,传递到下一个方法中,什么也不执行
self.__dfs(candidates, length, index + 1, path, residue - candidates[index], res)
path.pop()
Java 代码:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
public class Solution {
private List<List<Integer>> res = new ArrayList<>();
private int[] candidates;
private int len;
// residue 表示剩余,这个值一开始等于 target,基于题目中说明的"所有数字(包括目标数)都是正整数"这个条件
// residue 在递归遍历中,只会越来越小
private void findCombinationSum2(int residue, int begin, Stack<Integer> stack) {
if (residue == 0) {
res.add(new ArrayList<>(stack));
return;
}
for (int i = begin; i < len && residue - candidates[i] >= 0; i++) {
// 这一步之所以能够生效,其前提是数组一定是排好序的,这样才能保证:
// 在递归调用的统一深度(层)中,一个元素只使用一次。
// 这一步剪枝操作基于 candidates 数组是排序数组的前提下
if (i > begin && candidates[i] == candidates[i - 1]) {
continue;
}
stack.add(candidates[i]);
// 【关键】因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i
findCombinationSum2(residue - candidates[i], i + 1, stack);
stack.pop();
}
}
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
int len = candidates.length;
if (len == 0) {
return res;
}
this.len = len;
// 先将数组排序,这一步很关键
Arrays.sort(candidates);
this.candidates = candidates;
findCombinationSum2(target, 0, new Stack<>());
return res;
}
public static void main(String[] args) {
int[] candidates = {2, 5, 2, 1, 2};
int target = 5;
Solution solution = new Solution();
List<List<Integer>> combinationSum2 = solution.combinationSum2(candidates, target);
System.out.println(combinationSum2);
}
}
(本节完)