leetcode 组合总和系列问题(Python实现)

本文详细介绍了LeetCode上四个与组合总和相关的Python解法,包括组合总和I、II、III及IV。核心是利用回溯法解决这类问题,关键点在于处理数组中重复数字的使用限制,以及在不同场景下的调整。对于组合总和IV,采用了动态规划的方法求解组合个数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题描述

  • “组合总和”问题就是给定一个数组和一个目标数字,求出数组中的和为这个目标数字的子数组的集合。
  • 这类问题本质就是使用“回溯”的方法解决,但因为设定条件的不同,解法上有一定差异。
  • leetcode上目前有4个“组合总和”问题,下面一一介绍它们的解法

leetcode 39. 组合总和

  • 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
  • 其中,candidates中的数字可以被重复选取
  • 也就是说当candidates=[1, 2], target=3 时,[1,1,1]也是一个符合条件的答案,最终应输出[[1,2], [1,1,1]]
  • 代码:
class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        def traceback(res, path, curr_sum, pos):
            if curr_sum == target:
                res.append(path[:])
                return
            
            for i in range(pos, len(candidates)):
                if curr_sum + candidates[i] > target:
                    break
            
                path.append(candidates[i])
                traceback(res, path, curr_sum+candidates[i], i)
                path.pop()
        res, path = [], []
        candidates.sort()
        traceback(res, path, 0, 0)
        return res
  • 解释:
    • 以上代码中用traceback函数模拟了回溯的过程,参数res就是最终结果, path是当前的路径,curr_sum是当前的和,pos是当前递归的在数组中的位置
    • traceback函数的执行流程:
      • 先判断当前值是否等于目标,如果相等则把路径加入结果中,path[:]实际上构造了一个新的列表
      • for循环中:
        • 如果curr_sum + 当前值大于目标值,直接放弃当前路径
        • 把当前值加入路径中,进入递归:traceback(res, path, curr_sum+candidates[i], i),这里传的参数是i而不是i+1, 因为题目说数组中的数字可以重复使用。
        • path.pop() 回溯,选择下一个候选数字
    • 注意:
      • 我们要先对数组进行排序,这样可以减少回溯的次数并且避免重复的组合

leetcode 40. 组合总和 II

  • 给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
  • candidates 中的每个数字在每个组合中只能使用一次。
  • 先总结下这题和上一题的差异:
    • 数组中存在重复的数字
    • 每一个数字只能使用一次
  • 这题的代码整体上和上一题差不多,但是为了实现“每一个数字只能使用一次”,需要略加改动
  • 代码:
class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        
        def traceback(res, path, k, curr_sum):
            if curr_sum == target:
                res.append(path[:])
                return
            if curr_sum > target:
                return
            for i in range(k, len(candidates)):
                if candidates[i] > target-curr_sum:
                    break
                # 跳过这个数
                if i>k and candidates[i] == candidates[i-1]:
                    continue
                path.append(candidates[i])
                traceback(res, path, i+1, curr_sum+candidates[i])
                path.pop()
        res, path = [], []
        candidates.sort()
        traceback(res, path, 0, 0)
        return res
  • 解释:

和上一题代码的区别:

  1. 在for循环内部,加上了一个判断条件,这是最关键的地方:
  if i>k and candidates[i] == candidates[i-1]:
     continue
  1. 在进入递归时,位置是i+1而不是i,因为不能选择

下面我们举一个最简单的例子来说明

  • candidates=[1,1,2] target=2

如果直接套用上一题的代码?

  • 如果直接套用上一题的代码,我们得到的结果是:[[1,1], [1,1], [1,1], [2]]
    • [1,1] 出现了3次!然而这些[1,1]的来源是不同的:
      • 第一个[1,1], 两个1都是原数组0位置上的
      • 第二个[1,1],第一个1是原数组0位置上的,第二个1是原数组1位置上的
      • 第三个[1,1],两个1都是原数组1位置上的
    • 显然只有第二种是符合题意的
    • 用图表展示出来是这样的
      在这里插入图片描述
  • 根据上面这张表,只有路径B和F是符合题意的,我们如何排除路径A呢?
  • 正如上面提到的,递归时,索引加1即可,就不会选择到相同位置上的数字:
traceback(res, path, i+1, curr_sum+candidates[i])
  • 我们如何排除D呢?
  • D和E这组分支的产生是因为选择了不同位置上的相等的数字而形成的,这类的分支应当完全被避免,也就是通过以下代码完成:
if i>k and candidates[i] == candidates[i-1]:
    continue
  • break和continue?
    • break就相当于提前结束当前路径
    • continue相当于跳过当前数字,从下一个数字开始搜索

leetcode 216. 组合总和 III

  • 找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
  • 这题选择1-9的正整数,其实也就是在列表: list(range(1, 10))中选择,对比第1题(题号39)的差异在于:
    • 加入数字范围,即1-9
    • 加入了子数组数字个数限制
  • 此题的解法和第1题基本一致,只是稍作修改:
    1. 判断当前路径是否符合题意时,加上对个数的判断:
    if curr_sum == target:
      if len(path)==k:
      	res.append(path[:])
      return
    
    1. for循环的条件从:
      for i in range(pos, len(candidates)): ...
      变成:
      for i in range(pos, 10): ...

leetcode 377. 组合总和 Ⅳ

  • 给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
  • 请注意,顺序不同的序列被视作不同的组合。
  • 和前面的题不同,此题要求返回组合个数而不是结果集合

思路

  • 这题可用动态规划的方法求解
  • 转移方程:
    • 设定dp[i]为当目标数字为i时,符合条件的组合个数
    • 分析一下dp[i]和dp[0]到dp[i-1]的关系,以数组[1,1,2,3,5] 目标数字为4为例:
      • 假设我们要求出dp[4],即数组中组成4有几种组合,
      • 那么,我们可以看看数组中小于等于4的数有哪些,是[1,1,2,3]
      • 所以dp[4] 应等于dp[4-1] + dp[4-1] + dp[4-2] + dp[4-3]
      • 也就是:dp[3] + dp[3] + dp[2] + dp[1]
  • 初始状态:
    • 数组中是可能存在4这个数的,所以要求出dp[4]的时候,按照上面写出的转移方程,我们会需要dp[4-4]也就是dp[0],
    • 然而数组中全都是正整数,dp[0]没有意义,此时,我们要使dp[0]=1,即数组中恰有一个数等于目标数字,组合方案有1种
  • 结果:
    • dp[-1]就是最后要返回的答案
      代码:
class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        if len(nums) == 0:
            return 0

        dp = [0] * (target + 1)
        dp[0] = 1
        for i in range(1, target+1):
            for j, num in enumerate(nums):
                if num <= i:
                    dp[i] += dp[i-num]
        return dp[-1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值