题目
思路
套模板,回溯法
def backtrack(选择列表,路径):
if 满足结束条件:
操作res
return
for 选择 in 选择列表:
if 选择不合法
# 剪枝
continue
# 做选择 进入下一层决策树
选择列表.remove(选择)
路径.add(选择)
backtrack(选择列表,路径)
# 撤销选择
路径.remove(选择)
代码
回溯做法
class Solution {
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
// 记录「路径」
LinkedList<Integer> track = new LinkedList<>();
backtrack(candidates, target, track);
return res;
}
public void backtrack(int[] candidates, int target, LinkedList<Integer> track) {
// 结束条件
if (target == 0) {
res.add(new LinkedList(track));
return;
}
for (int candidate : candidates) {
// 剪去不合法的分支
if (candidate > target) {
continue;
}
// 做选择
track.add(candidate);
// 进入下一层决策树
backtrack(candidates, target - candidate, track);
// 取消选择
track.removeLast();
}
}
}
上面这段代码的输出是有问题的,尽管能拿到正确的组合,但是这里有一个顺序的问题。
如图,[2,2,3]和[2,3,2]代表的是同一个答案,但却同时出现了。
说明还需要有必要的剪枝,即进入下一次选择时,必须只能从比这次选择value大的选择中取。
改进一下,用一个index来记录,实现往后做选择。
public class Solution {
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
// 记录「路径」
LinkedList<Integer> track = new LinkedList<>();
backtrack(candidates, target, 0, track);
return res;
}
public void backtrack(int[] candidates, int target, int index, LinkedList<Integer> track) {
// 结束条件
if (target == 0) {
res.add(new LinkedList(track));
return;
}
for (int i = index; i < candidates.length; i++) {
// 剪去不合法的分支
if (candidates[i] > target) {
continue;
}
// 做选择
track.add(candidates[i]);
// 进入下一层决策树
backtrack(
candidates,
target - candidates[i],
i,
track
);
// 取消选择
track.removeLast();
}
}
}
动态规划
其实这道题不推荐使用动态规划来做,时间复杂度比较高,而且思路没有回溯这么清晰明确。
思路就是化为子问题,找状态转移方程嘛。
这里贴一个实现的示例。
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
Map<Integer, Set<List<Integer>>> map = new HashMap<>();
//对candidates数组进行排序
Arrays.sort(candidates);
int len = candidates.length;
for (int i = 1; i <= target; i++) {
//初始化map
map.put(i, new HashSet<>());
//对candidates数组进行循环
for (int j = 0; j < len && candidates[j] <= target; j++) {
if (i == candidates[j]) {
//相等即为相减为0的情况,直接加入set集合即可
List<Integer> temp = new ArrayList<>();
temp.add(i);
map.get(i).add(temp);
} else if (i > candidates[j]) {
//i-candidates[j]是map的key
int key = i - candidates[j];
//使用迭代器对对应key的set集合进行遍历
//如果candidates数组不包含这个key值,对应的set集合会为空,故这里不需要做单独判断
for (Iterator iterator = map.get(key).iterator(); iterator.hasNext(); ) {
List list = (List) iterator.next();
//set集合里面的每一个list都要加入candidates[j],然后放入到以i为key的集合中
List tempList = new ArrayList<>();
tempList.addAll(list);
tempList.add(candidates[j]);
//排序是为了通过set集合去重
Collections.sort(tempList);
map.get(i).add(tempList);
}
}
}
}
result.addAll(map.get(target));
return result;
}
结语
- 排列问题,讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为不同列表时),需要记录哪些数字已经使用过,此时用 used或者也叫visited 数组;
- 组合问题,不讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为相同列表时),需要按照某种顺序搜索,此时使用 begin 变量。
比较适用回溯算法的题还有这些,冲冲冲。