目录
题目要求
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入:candidates = [ 2,3,6,7 ], target = 7
输出:[ [ 2,2,3 ], [ 7 ] ]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:输入: candidates = [ 2,3,5 ], target = 8
输出: [ [ 2,2,2,2 ], [ 2,3,3 ], [ 3,5 ] ]
示例 3:输入: candidates = [ 2 ], target = 1
输出: [ ]
提示:
1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都 互不相同
1 <= target <= 500来源:力扣(LeetCode)
看到这道题很多读者如果熟悉的话就会想到——回溯,这个方法。其实跟之前讲过的一篇 “ 生成括号” 的文章方法相同,都是通过回溯来进行的。出于之前对回溯法的讲解过于简略,导致很多人可能并不是很清楚这个方法到底该如何使用,以及什么时候使用。因此本篇在对本题讲解的基础上也会对回溯法进行更细的一些讲解。
何为回溯法?
1. 很简单的理解就是排列组合,每一次进行的时候将本次能出现的结果依次放入,来判断到底哪些组合符合条件,然后在接着往下进行此步骤,以此类推。
2. 还有人将其解释为:DFS + 剪枝 = 回溯 。DFS是一种深度优先搜索算法,而 剪枝 则是一种判定条件,将不符合条件的数据去除从而简化程序运行。
3. 无论哪种解释其实说白了,回溯法就好比是给一棵树进行整理,DFS 就是对这个树进行深度的搜索,搜索条件然后执行。但是如果仅仅这样,那就是典型的暴力解法,因此 剪枝 的出现使其简化了程序的运行,从而使其得以保存 回溯 的名称。
对于回溯法有一个模板,这个将放在下面的代码讲解中进行讲解。
题目理解以及思路分析
(一) 明白了什么是 回溯法 后,我们再来了解本题,先来看看题目的要求。很简单,就是在给出的数组中找到任意的一个数字使其相加之和等于 target 的值。相信很多读者对于这样的问题都有一个思路,但是本题的难点在于 数组中的数字可以重复使用,这就使得传统的 for 循环无法直接用来解题,必须加入一些判定条件才行。
(二) 说的简单,但是到底应该加入什么条件呢?这里我们就用回溯法来解决,我们仔细思考一下,对于传统的求数组之和是怎么样实现的?很简单,就是在 for 循环中加入 sum += a[ i ] ,这一步就可。但是对于本题来说不能这么实现,因为每个数字可以重复使用。这样一来我们就多了一个判定——保证在 i 自增前,仅仅通过 a[ i ] 的有限个数相加能不能等于 target ,如果加到第一次出现 sum > target 的情况则说明 target 不是 a[ i ] 的整数倍,因此此时需要回溯(sum = sum - a[ i ])并且 i++ ,然后接着进行前一次的方法然后判定,以此类推。
为了便于理解,我做了下示图解:
代码分部讲解
第一部分
//全局定义一些变量
int* length;
int** ans;
int* path;
int path_top;
int result_top;
//定义函数进行回溯
void dfs(int* candidates, int candidatesSize, int target, int sum, int index)
{
int i, j; //定义变量
if(sum == target) //符合条件
{
int* temp = malloc(sizeof(int) *path_top); //分配空间
//将符合条件的数据赋给 temp 数组
for(i = 0; i < path_top; i ++)
{
temp[i] = path[i];
}
ans[result_top] = temp; //将一维数组赋值给 ans 数组
length[result_top++] = path_top; //计算长度
return;
}
//剪枝
for(j = index ; sum < target && j < candidatesSize; j++)
{
sum += candidates[j];
path[path_top++] = candidates[j];
dfs(candidates, candidatesSize, target, sum, j); //回溯
sum -= candidates[j];
path_top --;
}
}
这一部分比较多,而其是整个程序中最为重要的一部分,因此这里会用很多的篇幅进行讲解:
先来讲解——剪枝部分的代码
for(j = index ; sum < target && j < candidatesSize; j++) { sum += candidates[j]; path[path_top++] = candidates[j]; dfs(candidates, candidatesSize, target, sum, j); //回溯 sum -= candidates[j]; path_top --; }
上面已经讲过了,为了使得回溯更加的简便因此就要加入 剪枝 的部分来排除一些数据,很显然这里剪枝的条件是 sum < target && j < candidatesSize ;
设置了剪枝限制,下面就是处理剩余的数据了。
这里需要特别注意一点:
path[path_top++] = candidates[j];
这个其实代表了两部分,拆开来看如下所示:
path[path_top] = candidates[j]; path_top ++;
这样,很多读者就看明白了。其实这里就是用 path 数组来储存每一次的数据,这样就保证了可以在一次 i 循环中实现一个数据重复使用的过程。
而这里的 path_top 其实就是用来计算数组 path 中的长度的(即 path 数组中储存的数据的数量)
dfs(candidates, candidatesSize, target, sum, j); //回溯 sum -= candidates[j]; path_top --;
相信细心的朋友也发现了这里,其实这个就是我前面想说的 回溯法的模板
void DFS(.........){ if(判断条件){ ...... .... .. } for(剪枝条件){ ...... depath++; .. DFS(........) //回溯 depath--; }
回溯后,必须将之前有影响的数据进行消除,以此来保证可以正常进行回溯;
PS: 任何的模板都只是个方法,其出现的目的都只是便于理解。而非是固化的去用,很多时候需要变通,因此不要让模板来固化我们的思维,要懂得举一反三,灵活运用,以不变应万变。
讲完了剪枝部分的,下面就讲讲符合条件的数据该如何处理:
void dfs(int* candidates, int candidatesSize, int target, int sum, int index) { int i, j; //定义变量 if(sum == target) //符合条件 { int* temp = malloc(sizeof(int) *path_top); //分配空间 //将符合条件的数据赋给 temp 数组 for(i = 0; i < path_top; i ++) { temp[i] = path[i]; } ans[result_top] = temp; //将一维数组赋值给 ans 数组 length[result_top++] = path_top; //计算长度 return; }
这一部分就相对于来说较简单了。
for(i = 0; i < path_top; i ++) { temp[i] = path[i]; } ans[result_top] = temp;
这一部分相信看完上面的讲解应该大都明白了,其实这部分就是将剪枝剩余的符合条件的数据重新用 temp 数组进行一个储存。
最后将整个 temp 数组全部赋值给 ans 数组;
length[result_top++] = path_top;
这一步,可能很多读者不明白,认为这一步完全没必要,但是我想说这一步至关重要。这一步也可以拆开:
length[result_top] = path_top; result_top++
length 就是用来计算每次 ans[ result_top ] 对应的数组长度,用于后面的申请动态空间以及返回值的长度限制。
第二部分
int** combinationSum(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes){
path_top = 0; //从零开始
result_top = 0; //从零开始
//申请动态空间
path = malloc(sizeof(int) *100);
ans = malloc(sizeof(int*) *150);
length = malloc(sizeof(int) *150);
dfs(candidates,candidatesSize,target,0,0); //调用函数
*returnSize = result_top; //返回数组的行数
*returnColumnSizes = malloc(sizeof(int) *150); //申请动态空间
//对每一个行数进行其对应的列数限制
for(int i =0 ; i<result_top ; i++)
{
(*returnColumnSizes)[i] = length[i];
}
return ans;
}
//对每一个行数进行其对应的列数限制 for(int i =0 ; i<result_top ; i++) { (*returnColumnSizes)[i] = length[i]; }
这一步就提现到了上述步骤里的
length[result_top++] = path_top;
因此,这一步必不可少!!!
第三部分
附上完整的代码:
int* length;
int** ans;
int* path;
int path_top;
int result_top;
void dfs(int* candidates, int candidatesSize, int target, int sum, int index)
{
int i, j;
if(sum == target)
{
int* temp = malloc(sizeof(int) *path_top);
for(i = 0; i < path_top; i ++)
{
temp[i] = path[i];
}
ans[result_top] = temp;
length[result_top++] = path_top;
return;
}
for(j = index ; sum < target && j < candidatesSize; j++)
{
sum += candidates[j];
path[path_top++] = candidates[j];
dfs(candidates, candidatesSize, target, sum, j);
sum -= candidates[j];
path_top --;
}
}
int** combinationSum(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes){
path_top = 0;
result_top = 0;
path = malloc(sizeof(int) *100);
ans = malloc(sizeof(int*) *150);
length = malloc(sizeof(int) *150);
dfs(candidates,candidatesSize,target,0,0);
*returnSize = result_top;
*returnColumnSizes = malloc(sizeof(int) *150);
for(int i =0 ; i<result_top ; i++)
{
(*returnColumnSizes)[i] = length[i];
}
return ans;
}
总结
本篇对回溯法进行更加详细的介绍,便于之前对此不明白的读者更好的理解和运用该方法。本篇也给出了回溯法的使用模板,希望大家根据模板来理解题目,但是不要固化思维,灵活运用才是正解。多多理解本篇文章,下一篇我们将讲解的是——全排列(稍微透露一下:也是用的回溯法) 有兴趣的大家可以先自己去了解了解。