Backtracking有很多很多不同场景的问题。Backtracking其实就是DFS + 剪枝。就几乎是枚举,然后either发现满足条件的branch,or当前branch已经不可能符合条件了剪枝放弃。
写一下经典排序组合问题吧。
题目 | 简介 | candidates 是否有重复元素 | 元素是否允许 重复使用 | 有start |
---|---|---|---|---|
39. Combination Sum | 求和为k的组合 | 元素distinct | 允许多次使用 | 有 |
40. Combination Sum II | 求和为k的组合 | 元素可以有重复 | 有几个限几个 | 有 |
78. Subsets | 求所有子集 | 元素distinct | 有啥用啥 | 有 |
90. Subsets II | 求所有子集 | 元素可以有重复 | 有几个限几个 | 有 |
46. Permutations | 求所有排列 | 元素distinct | - - (全用) | 无 |
47. Permutations II | 求所有排列 | 元素可以有重复 | - - (全用) | 无 |
77. Combinations(类78) | 所有sizeK子集 | 元素distinct | 用k个 | 有 |
- 组合:需要start,来限制当前层只能从一个范围内挑选(而不是全集),因为有些已经使用过了。组合问题是从“当前resource是否使用”的角度考虑的,resource1,可以用也可以不用,决定之后,resource2,可以用也可以不用,……等到当前层的时候,比如resourceM,那M之前的都已经考虑过了(并且已经做出决定了),于是当前层就只考虑从M到最后这部分resource即可了(即从start开始)。
- 排序:不需要start。因为排序问题是从“当前position有哪些合法candidate”的角度考虑的。而candidate(即resource)是按照resource排序的,组合问题从“当前resource是否使用”角度考虑,肯定是按顺序的,于是可以用start来标记,而这里只能用一整个数组used[] 来记录哪些resource已经使用过了。
39. Combination Sum
Input: candidates = [2,3,6,7], target = 7,
A solution set is:
[
[7],
[2,2,3]
] candidates中元素可以用无数次
这个题candidates = [2,3,6,7]里的元素允许无限次使用,若求所有组合,则是无限多个的。好在还有个和target=7的限制。
因为candidates元素允许无限次使用,所以往下一级传start的时候,不是传i+1,而是传i,下一级仍然处理当前位。那岂不是在这一位一直不往下一位跳吗?对就是这样,本来2,2,2,……就是其中一个branch,直到总和超过target,不符合要求了,被剪枝。
关于最开头candidates是不是需要先排序这个问题,如果candidates元素distinct,那就不需要排序。排序都存在于“candidates可能有重复元素”的情况,要保证值相同的两个元素挨着。元素本身的值大小关系,我们不关心。
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> lst = new ArrayList<>();
genCombination(lst, new ArrayList<Integer>(), candidates, target, 0);
return lst;
}
private void genCombination(List<List<Integer>> lst, List<Integer> path, int[] candidates, int remain, int start) {
if (remain == 0) {//good terminal
lst.add(new ArrayList<>(path));//注意:new一个新的ArrayList
return;
} else if (remain < 0) {//bad terminal
return;
}
for (int i = start; i < candidates.length; i++) {
path.add(candidates[i]);
genCombination(lst, path, candidates, remain-candidates[i], i);
path.remove(path.size()-1);
}
}
}
40. Combination Sum II
Input: candidates = [10,1,2,7,6,1,5], target = 8,
A solution set is:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
] 区别:candidates中每个元素只能用一次
- 39: candidates元素允许无限次使用,往下一级传start的时候传i。
- 40: candidates元素只允许使用一次,往下一级传start的时候传i+1。
这里candidates可能有重复元素,于是需要在backtracking之前先排序[1,1,2,5,6,7,10],保证值相同的元素挨着。挨着之后,我们就要想“重复元素是否需要跳过”。注意不是所有值相同的元素都跳过,[1,1,2,…]这里在处理第二个1的时候,不能跳过,否则 [1, 1, 6]就漏掉了。
- 这里说的“跳过重复”是指除了当前start所在位以外,后面的数里如果存在重复,比如[1,1,2,5,6,6,7,10],第二个6就跳过不处理了(同层横向–跳过)。
- 而“上一层的1和本层的1”这种不跳过(上下层纵向–不跳过)。
用 i > start && candidates[i] == candidates[i-1] 来定义跳过条件,把i = = start,即本层第一个排除出去(不跳过)。
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
List<List<Integer>> lst = new ArrayList<>();
List<Integer> path = new ArrayList<>();
getCombination(lst, path, candidates, target, 0);
return lst;
}
private void getCombination(List<List<Integer>> lst, List<Integer> path, int[] candidates, int remain, int start) {
if (remain == 0) {//good terminal
lst.add(new ArrayList<>(path));
return;
} else if (remain < 0) {//bad terminal
return;
}
for (int i = start; i < candidates.length; i++) {
if (i > start && candidates[i] == candidates[i-1]) {continue;}
path.add(candidates[i]);
getCombination(lst, path, candidates, remain-candidates[i], i+1);
path.remove(path.size()-1);
}
}
}
78. Subsets
Input: nums = [1,2,3], Input元素distinct
Output:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
39,40 combination sum是以“和达到target”作为一个branch的成功条件。78,90 subset是没有鉴别标准,要求所有的branch。这种情况就必须要求“不能允许无限次使用元素”,否则就没完了。
78元素distinct,90允许有重复元素,但都是某个值有几个就最多使用几个。所以从“当前resouce是否使用”的角度考虑,这个用过了就不能再用了,往下一层传传i+1。
78元素distinct,就不存在排序的需要,也没有跳不跳过的问题(只有有重复元素且排序过的才需要考虑跳过)。
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
getSubset(list, new ArrayList<Integer>(), nums, 0);
return list;
}
private void getSubset(List<List<Integer>> list, List<Integer> path, int[] nums, int start) {
list.add(new ArrayList<>(path));//无论如何都把path添加进list
for (int i = start; i < nums.length; i++) {
path.add(nums[i]);
getSubset(list, path, nums, i+1);
path.remove(path.size()-1);
}
}
}
90. Subsets II
Input: [1,2,2] 不同:Input可能有重复元素
Output:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
几乎和78一样,除了78元素distinct,90允许有重复元素。需要排序,且需要考虑跳过的问题。跳过的方法和40. Combination Sum II一样。
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> lst = new ArrayList<>();
Arrays.sort(nums);
getSubset(lst, new ArrayList<Integer>(), nums, 0);
return lst;
}
private void getSubset(List<List<Integer>> lst, List<Integer> path, int[] nums, int start) {
lst.add(new ArrayList<>(path));
for (int i = start; i < nums.length; i++) {
if (i > start && nums[i] == nums[i-1]) {continue;}
path.add(nums[i]);
getSubset(lst, path, nums, i+1);
path.remove(path.size()-1);
}
}
}
46. Permutations
Input: [1,2,3],数组元素distinct
Output:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
上面四个都是组合,这个是排列,因为所有元素都要用,取用元素和input的顺序不一定相同,所以无法用start来限制可选范围。
那怎么知道哪些元素已经用过了呢?path.contains(nums[i]) 作为“检查是否已使用过”条件。因为各个元素值不同,所以直接在ArrayList里检查是否已经出现过就可以了。
此处注意:“检查是否已使用过”条件和“跳过”条件不同。参见下面47的解释。
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> ret = new ArrayList<>();
backtracking(ret, new ArrayList<>(), nums);
return ret;
}
private void backtracking(List<List<Integer>> list, List<Integer> path, int[] nums) {
if (path.size() == nums.length) {
list.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (path.contains(nums[i])) {continue;}
path.add(nums[i]);
backtracking(list, path, nums);
path.remove(path.size()-1);
}
}
}
47. Permutations II
Input: [1,1,2] 区别:input数组可能有重复的
Output:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
- 46: “检查是否已使用过”条件:path.contains(nums[i])。因为各个元素值不同,所以直接在ArrayList里检查是否已经出现过就可以了。
- 47: “检查是否已使用过”条件:used[i] == true。因为有可能有值相同的元素,所以需要单独一个数组used[]来记录每个元素是否已经使用过。
“检查是否已使用过”条件和“跳过”条件不同。
47还有一个“跳过”条件:和40. Combination Sum II,90. Subsets II类似。
i > 0 && nums[i] == nums[i-1] && !used[i-1]
和40,90的区别在于:
- 40,90: i = = start 不跳过,即每层的第一个元素如果和上一层元素相同,则不跳过。[1,1,2,5,6,7,10],即处理[1,1]这里上下级调用的时候,第二个1不跳过。
- 47: used[i-1]= = true 不跳过,即相同的那两个元素中的former那个元素已经用过,则latter那个不跳过。
其实这两种情况本质上是一样的。都是:former若还没用,则latter跳过(因为everything都在former处处理),former已经用了,那latter不能跳过,[1]和[1,1]都需要考虑。(40,90因为顺序处理,所以i= =start就是“former已经用了,那latter不能跳过”的情况)。
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> lst = new ArrayList<>();
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
getPermutation(lst, new ArrayList<Integer>(), nums, used);
return lst;
}
private void getPermutation(List<List<Integer>> lst, List<Integer> path, int[] nums, boolean[] used) {
if (path.size() == nums.length) {
lst.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) {continue;}//跳过条件
if (used[i] == true) {continue;}//检查是否已使用过
path.add(nums[i]);
used[i] = true;
getPermutation(lst, path, nums, used);
used[i] = false;
path.remove(path.size()-1);
}
}
}
77. Combinations
Input: n = 4, k = 2
Output:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
和78. Subsets几乎一样,除了这里只有长度为2的path才认为是成功的(才添加进result list里)。这里不贴代码啦。