(LeetCode) 组合总和——回溯法

目录

题目要求

何为回溯法?

题目理解以及思路分析

代码分部讲解

第一部分

第二部分

第三部分

总结


题目要求

给你一个 无重复元素 的整数数组 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;
}

总结

       本篇对回溯法进行更加详细的介绍,便于之前对此不明白的读者更好的理解和运用该方法。本篇也给出了回溯法的使用模板,希望大家根据模板来理解题目,但是不要固化思维,灵活运用才是正解。多多理解本篇文章,下一篇我们将讲解的是——全排列(稍微透露一下:也是用的回溯法) 有兴趣的大家可以先自己去了解了解。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

简十三

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值