LeetCode
https://leetcode.com/problems/combination-sum/description/
39. Combination Sum
Given a set of candidate numbers (C) (without duplicates) and a target number (T), find all unique combinations in C where the candidate numbers sums to T.
The same repeated number may be chosen from C unlimited number of times.
Note:
All numbers (including target) will be positive integers.
The solution set must not contain duplicate combinations.
For example, given candidate set [2, 3, 6, 7] and target 7,
A solution set is:
[
[7],
[2, 2, 3]
]
1.回溯算法
回溯法按深度优先策略搜索问题的解空间树。首先从根节点出发搜索解空间树,当算法搜索至解空间树的某一节点时,先利用剪枝函数判断该节点是否可行(即能得到问题的解)。如果不可行,则跳过对该节点为根的子树的搜索,逐层向其祖先节点回溯;否则,进入该子树,继续按深度优先策略搜索。
回溯法的基本行为是搜索,搜索过程使用剪枝函数来为了避免无效的搜索。剪枝函数包括两类:
1. 使用约束函数,剪去不满足约束条件的路径;
2. 使用限界函数,剪去不能得到最优解的路径。
问题的关键在于如何定义问题的解空间,转化成树(即解空间树)。解空间树分为两种:子集树和排列树。两种在算法结构和思路上大体相同。
对于本题我们的目标非常明确,就是利用剪枝函数剪掉不满足约束条件的值。首先定义问题的解空间树。
由上图解题思路就非常明确了。下面是代码实现。
/*
* backtrack
* backtrack函数中nums表示当前的combination,remain表示combination的和与target的数值差。
* index表示当前遍历cans的位置,是为了保持nums中从大到小的序列从而确保唯一性而设置的,
* numcount是nums中有效元素的界限位置。
* */
public static List<List<Integer>> combinationSum(int[] candidates, int target){
List<List<Integer>> result = new ArrayList<>();
int[] length = new int[target];
Arrays.sort(candidates);
backtrack(result,length,candidates,target,0,0);
return result;
}
public static void backtrack(List<List<Integer>> result,int[] nums,int[] cans,int remain,int index,int numcount){
if(remain == 0){
List<Integer> list = new ArrayList<>();
for (int i = 0; i < numcount; i++) {
list.add(nums[i]);
}
result.add(list);
return;
}else if(remain < 0){
return;
}else if(remain>0){
for (int i = index; i < cans.length; i++) {
nums[numcount] = cans[i];
backtrack(result,nums,cans,remain-cans[i],index,numcount+1);
index++;
}
}
}
2.动态规划算法
动态规划算法基本思想与分治法类似,也是将待求解的问题分解为若干个子问题,按一定的顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
本问题是求解和为一个指定target的所有集合。我们可以把他分解为求target为1,2,3…的子问题。
实现代码如下:
其中ksum用于求解target为k的解,每一次执行ksum时遍历cans,对于每一个cans元素,target则可以为cans和(target-cans[x])解集的和。,
/*
* dynamic program
* 1.求和为target的List分解为和为1-target的list。(分解为子任务)
* 2.单步循环candidates得到和为所求sub-target的所有可能组成方式。
* 2.1 sub-target的combination在求解过程中list按asc顺序,避免防止重复。
* */
public static List<List<Integer>> combinationSumD(int[] candidates, int target) {
int k = target/candidates[0];
List<List<List<Integer>>> targetList = new ArrayList<>();
List<List<Integer>> list = new ArrayList<>();
targetList.add(list);
Arrays.sort(candidates);
for (int i = 1; i <=target ; i++) {
List<List<Integer>> kSum = kSum(targetList,candidates,i);
targetList.add(kSum);
}
return targetList.get(target);
}
public static List<List<Integer>> kSum( List<List<List<Integer>>> targetList,int[] candidates,int k){
List<List<Integer>> list = new ArrayList<>();
for (int i = 0; i < candidates.length; i++) {
if(candidates[i] > k){
break;
}else if(candidates[i] == k){
list.add(Arrays.asList(candidates[i]));
}else{
for(List<Integer> list1:targetList.get(k-candidates[i])){
if(candidates[i]>=list1.get(list1.size()-1)) {
List<Integer> tmp = new ArrayList<>(list1);
tmp.add(candidates[i]);
list.add(tmp);
}
}
}
}
return list;
}