给定一个数组 arr 和一个目标数 target ,找出 arr 中所有可以使数字和为 target 的组合。
arr 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: arr = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: arr = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
需要求出所有和为 target 的组合,并且每个数只能使用一次,因此可以使用递归 + 回溯的方法:
用 combinationSumIIDFS(pos,rest) 表示递归的函数,其中 pos 表示当前递归到了数组 arr 中的第 pos 个数,而 rest 表示还需要选择和为 rest 的数放入列表作为一个组合;
对于当前的第 pos 个数,有两种方法:选或者不选。如果选了这个数,那么调用 combinationSumIIDFS(pos+1,rest−arr[pos]) 进行递归,注意这里必须满足 rest≥arr[pos]。如果不选这个数,那么调用 combinationSumIIDFS(pos+1,rest) 进行递归;
在某次递归开始前,如果 rest 的值为 0,说明找到了一个和为 target 的组合,将其放入答案中。每次调用递归函数前,如果选了那个数,就需要将其放入列表的末尾,该列表中存储了选的所有数。在回溯时,如果选了那个数,就要将其从列表的末尾删除。
上述算法就是一个标准的递归 + 回溯算法,但是它并不适用于本问题求解。这是因为题目描述中规定了解集不能包含重复的组合,而上述的算法中并没有去除重复的组合。
因此,需要改进上述算法,在求出组合的过程中就进行去重的操作。考虑将相同的数放在一起进行处理,如果数 x 出现了 y 次,那么在递归时一次性地处理它们,即分别调用选择 0,1,⋯ ,y 次 x 的递归函数。这样就不会得到重复的组合。
使用一个哈希映射(HashMap)统计数组 arr 中每个数出现的次数。在统计完成之后,将结果放入一个列表 nums 中,方便后续的递归使用。列表 nums 的长度即为数组 arr 中不同数的个数。其中的每一项对应着哈希映射中的一个键值对,即某个数以及它出现的次数。
在递归时,对于当前的第 pos 个数,它的值为 nums[pos][0],出现的次数为 nums[pos][1],那么可以调用 combinationSumIIDFS(pos+1,rest−i×nums[pos][0])
即选择了这个数 i 次。这里 i 不能大于这个数出现的次数,并且 i×nums[pos][0] 也不能大于 rest。同时,我们需要将 i 个 nums[pos][0] 放入列表中。
这样一来,就可以不重复地枚举所有的组合了。
package com.loo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class CombinationSumII {
public static List<int[]> nums = new ArrayList<int[]>();
public static List<List<Integer>> list = new ArrayList<List<Integer>>();
public static List<Integer> coms = new ArrayList<Integer>();
public static void main(String[] args) {
int[] arr1 = {2,3,6,7};
int target = 7;
List<List<Integer>> lists = getCombinationSumII(arr1 , target);
for (List<Integer> l : lists) {
System.out.println(l);
}
int[] arr2 = {2,3,5};
target = 8;
nums.clear();
list.clear();
coms.clear();
lists = getCombinationSumII(arr2 , target);
for (List<Integer> l : lists) {
System.out.println(l);
}
int[] arr3 = {10,1,2,7,6,1,5};
target = 8;
nums.clear();
list.clear();
coms.clear();
lists = getCombinationSumII(arr3 , target);
for (List<Integer> l : lists) {
System.out.println(l);
}
int[] arr4 = {2,5,2,1,2};
target = 5;
nums.clear();
list.clear();
coms.clear();
lists = getCombinationSumII(arr4 , target);
for (List<Integer> l : lists) {
System.out.println(l);
}
}
public static List<List<Integer>> getCombinationSumII(int[] arr , int target) {
if (arr == null || arr.length == 0) {
return list;
}
Arrays.sort(arr);
for (int n : arr) {
int len = nums.size();
if (nums.isEmpty() || n!=nums.get(len-1)[0]) {
nums.add(new int[]{n , 1});
} else {
++nums.get(len-1)[1];
}
}
combinationSumIIDFS(0 , target);
return list;
}
public static void combinationSumIIDFS(int pos , int target) {
if (target == 0) {
list.add(new ArrayList<Integer>(coms));
return;
}
if (pos == nums.size() || nums.get(pos)[0] > target) {
return;
}
combinationSumIIDFS(pos + 1 , target);
int times = Math.min(target/nums.get(pos)[0], nums.get(pos)[1]);
for (int i=1;i<=times;i++) {
coms.add(nums.get(pos)[0]);
combinationSumIIDFS(pos+1 , target-i*nums.get(pos)[0]);
}
for (int i=1;i<=times;i++) {
coms.remove(coms.size()-1);
}
}
}