给定一个数组 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); } }
}