用回溯(递归)解决组合问题
- 第一类组合问题
- 77.组合
- 216.组合问题Ⅲ
- 第二类组合问题
- 39. 组合总和
- 递归法一:组合位置填空
- 递归法二:遍历数组
- 40. 组合总和 II
- 递归法一:组合位置填空
- 递归法二:遍历数组
- ❌ 常规思路:有重复
- ❗hash表解决冲突
- 377. 组合总和 Ⅳ 【动态规划】
第一类组合问题
77.组合
77.组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
这个题目之前的博客已经解答过了,里面还有字典序/二进制序列法求解代码。这里再说一下回溯法。遍历[1~n]里面的每个数,每次都有两种两种情况:选or不选;递归结束的条件是已经有k个数字了or数组遍历完了,其实还有一个结束条件:当前遍历的数字中选择的有m个,还剩下p个,然而m+p < k了,继续遍历下去就算全部选择也不可能满足“k个数”这个条件。Java代码如下:
class Solution {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> temp = new ArrayList<Integer>();
public List<List<Integer>> combine(int n, int k) {
dfs(1, n, k);
return ans;
}
public void dfs(int cur, int n , int k){
if(temp.size() + (n - cur + 1) < k)
return ;
if(temp.size() == k){
ans.add(new ArrayList<Integer>(temp));
return ;
}
temp.add(cur);
dfs(cur + 1, n, k);
temp.remove(temp.size() - 1);
dfs(cur + 1, n, k);
}
}
216.组合问题Ⅲ
216. 组合问题Ⅲ
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
这和77的区别是,这个题目不仅要找k个数,还要求和为n。因此在上一个题的基础上,加上一个判断过程【和是否为n】即可:
class Solution {
List<Integer> temp = new ArrayList<Integer>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
public List<List<Integer>> combinationSum3(int k, int n) {
dfs(1, k, n);
return ans;
}
public void dfs(int cur, int k, int sum) {
if (temp.size() + (9 - cur + 1) < k) {
return;
}
if (temp.size() == k) {
int tempSum = 0;
for (int num : temp) {
tempSum += num;
}
if (tempSum == sum) {
ans.add(new ArrayList<Integer>(temp));
}
return;
}
temp.add(cur);
dfs(cur + 1, k, sum);
temp.remove(temp.size() - 1);
dfs(cur + 1, k, sum);
}
}
第二类组合问题
下面的几个题,可以认为是一个系列的~
39. 组合总和
39. 组合总和
给你一个 无重复元素 的整数数组
candidates
和一个目标整数target
,找出 candidates 中可以使数字和为目标数 target的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个数字可以无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
递归法一:组合位置填空
这个递归思路,是每次递归都要给这个位置选择一个数,这个数是candidates数组中的——即每次递归,都遍历取依次candidates中的数。不过取的数字需要满足一些条件。
- 我们肯定想先从小的数字开始选,毕竟题目给的答案也是从小的数字开始的。所以先对candidates排序;
- 每次递归都找一个数,但是这个数num需要小于等于target;【num都比target大了还怎么满足加上去和为target是吧】
- 根据这个的思路,第一个例子candidates =[2,3,6,7],第一次递归选2,第二次递归如果选3,第三次递归可以选2;那么和第一次递归选2,第二次递归选2,第三次递归选3就重复了;这里需要添加一个条件以去除重复:⭐我们的答案组合中的数字是从小到大的,因此我们每次递归选的数组 num 必须大于等于前一轮选的数字 ,假设为min,即num≥min;因为candidate是已经排序的,那么每一轮开始寻找的初始索引,应该是从上一轮选择的数的索引开始的;
- 递归结束条件就是传入的target为0;以及遍历完数组,待选择的数为空;
完整Java代码如下:【两种都可以,一个是直接传min;一个是传idx】
class Solution {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> temp = new ArrayList<Integer>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
dfs(candidates, target,0);
return ans;
}
public void dfs(int[] candidates, int target,int idx){
if(target == 0 ){
ans.add(new ArrayList<Integer>(temp));
return;
}
//candidates数组中已经没有数字可选以满足条件
if(idx == candidates.length)
return;
for(int i = idx ; i < candidates.length && candidates[i] <= target; i++){
temp.add(candidates[i]);
dfs(candidates, target - candidates[i], i);
temp.remove(temp.size() - 1);
}
}
}
class Solution {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> temp = new ArrayList<Integer>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates); //先排序,升序
dfs(candidates, target,candidates[0]); //初始传入的min即为排序后candidates中的第一个元素
return ans;
}
public void dfs(int[] candidates, int target,int min){
if(target == 0 ){
ans.add(new ArrayList<Integer>(temp));
return;
}
//candidates数组中已经没有数字可选以满足条件
if(target < min)
return;
for(int num : candidates){ //遍历candidate
if(num >= min && num <= target){ //选择满足条件的数字
temp.add(num);
dfs(candidates,target - num, num); // 更新target为target-num
temp.remove(temp.size()-1);
}
}
}
}
递归法二:遍历数组
第二个思路是去遍历数组,那么每次针对数组中的数字有两种情况:选or不选。
每一轮递归的目标是选择一个数num,使得这个数num与当前还差的target相等。递归结束条件①target=0,即选了这些数以后,与目标值还差0;即选的这些数和已经为target了!②遍历完了这个数组,因此需要一个参数记录遍历到了数组的位置/下标,idx。这个情况不需要先对candidates排序。
完整Java代码如下:
class Solution {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> temp = new ArrayList<Integer>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
dfs(candidates, target,0); //初始idx为0
return ans;
}
public void dfs(int[] candidates, int target,int idx){
if(target == 0 ){
ans.add(new ArrayList<Integer>(temp));
return;
}
//已经遍历完数组
if(idx == candidates.length)
return;
dfs(candidates,target,idx + 1); // 不选择这个数,跳过当前下标idx,下次从idx+1开始选
//如果当前num比target小,选择
if(target - candidates[idx] >=0){
temp.add(candidates[idx]);
//注:因为一个数可以选择多次,因此选择这个数后,idx不变化
dfs(candidates, target - candidates[idx], idx);
temp.remove(temp.size()-1);
}
}
}
40. 组合总和 II
40. 组合总和 II
给定一个候选人编号的集合
candidates
和一个目标数target
,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
递归法一:组合位置填空
这个题目和上面的区别在两点:
- 上一题candidates中的数字没有重复;这个题目candidates是有重复的;
- 上一题candidates中的数字可以无限次使用;这个题目candidates的数量是有限的;
组合位置填空法的遍历思路,还是每次遍历都要找一个数【在candidates中找一个】,但是这里需要增加一点以去除重复组合:比如candidates = [2,5,2,1,2]这个例子,先对candidates排序→candidates = [1,2,2,2,5];第一轮选择了1,第二轮选择[2,2,2,5]中的任意一个,那么第二个位置选2的就有3种情况【因为有3个2】,这就引起重复了。 去除重复:candidates已经排序,相等元素是相邻的,如果candidates[i] == candidates[i-1],说明这个candidates[i]不能再选了,但是i+1的情况知道,因此continue,后面的数字继续。
完整Java代码如下:
class Solution {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> temp = new ArrayList<Integer>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
dfs(candidates,target,0);
return ans;
}
public void dfs(int[] candidates, int target, int idx){
if(target == 0){ //找到满足要求的组合,递归结束
ans.add(new ArrayList<Integer>(temp));
return;
}
if(idx == candidates.length) //遍历完数组,递归结束
return;
// 遍历idx~ candidates.length-1的数,没有重复的就加入
for(int i = idx; i < candidates.length && target >= candidates[i]; i++){
if( i > idx && candidates[i] == candidates[i-1]) //重复了,跳过该数
continue;
temp.add(candidates[i]);
dfs(candidates, target - candidates[i], i+1);
temp.remove(temp.size()-1);
}
}
}
递归法二:遍历数组
上面一题,在遍历数组的递归过程中,选择一个数字后,由于这个数字可以重复使用,因此idx不变化。 那么是否这个题目,针对一个数有选or不选的情况;不管选or不选,idx+1,选下一个,是否正确?
问题就出在这个题目的candidates是重复的,还是candidates = [1,2,2,2,5],target = 5这个例子,三个2,不选第一个2+选第二个2,选第一个2+不选第二个2,实际是一样的。
❌ 常规思路:有重复
用上面的常规思路,写的代码:
class Solution {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> temp = new ArrayList<Integer>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
dfs(candidates,target,0);
return ans;
}
public void dfs(int[] candidates, int target, int idx){
if(target == 0){
ans.add(new ArrayList<Integer>(temp));
return;
}
if(idx == candidates.length)
return;
if(target >= candidates[idx]){
temp.add(candidates[idx]);
dfs(candidates, target - candidates[idx],idx+1);
temp.remove(temp.size()-1);
}
dfs(candidates,target,idx+1);
}
}
结果如下,有重复:
这个test case的结果中出现了两次[1,2,5]这是因为candidates中有两个1,组合第一位就有两种选择;同样[1,7]也重复。 但是[1,1,6]不重复,是两个1都用上了!
这个test case的结果中出现了三次[1,2,2]是因为candidates中有3个2,结果中的两个2,可以是(第一个2,第二个2)、(第一个2,第三个2)、(第二个2,第三个2)。
❗hash表解决冲突
这个解决冲突的思路,就是先把candidates去重。统计candidates中的数及出现的次数,存在freq中。那么freq中的每个数有选or不选,两种情况;针对选的情况,又有选几次的问题:min(target/num, count),即现在的target最多能选几个num、这个num在candidates中有一个,二者的最小值。 完整java代码如下:
class Solution {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> temp = new ArrayList<Integer>();
List<int[]> freq = new ArrayList<int[]>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
// 先统计candidates中的数及出现的频率
for(int num : candidates){
int size = freq.size();
if(freq.isEmpty() || num != freq.get(size-1)[0])
freq.add(new int[]{num,1});
else
freq.get(size-1)[1]++;
}
dfs(target,0);
return ans;
}
public void dfs(int target, int idx){
if(target == 0){
ans.add(new ArrayList<Integer>(temp));
return;
}
// 已经遍历完freq中的数 或者 当前freq中的数已经大于targetl直接剪枝
if(idx == freq.size() || target < freq.get(idx)[0])
return;
// 依次选择1~ target/num个num这个数
int num = freq.get(idx)[0];
int most = Math.min(target/num, freq.get(idx)[1]);
for(int i = 1; i <= most; i++){
temp.add(num);
dfs(target - i*num, idx + 1);
}
// 选择后要移除掉
for(int i = 1; i <= most; i++)
temp.remove(temp.size()-1);
// 这个数字一个都不选!
dfs(target, idx + 1);
}
}
377. 组合总和 Ⅳ 【动态规划】
377. 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
这个题目虽然是一系列的变化题目…但是需要用动态规划求解。对于数字k,遍历nums数组中比k小的其他元素j,dp[k] += dp[k-j]。 其中dp[0] = 1, 表示k == num的情况,只有这一种组合。那么完整版Java代码如下:
class Solution {
public int combinationSum4(int[] nums, int target) {
int n = nums.length;
int[] count = new int[target+1];
count[0] = 1;
Arrays.sort(nums);
for(int i = 1; i <= target; i++){ //从小到大依次计算i的组合情况
for(int j = 0; j < n && nums[j] <= i; j++) // 遍历nums数组中的元素,加入组合
count[i] += count[i - nums[j]];
}
return count[target];
}
}