返回

每日一道算法题,解决“组合总和”问题

前端

理解组合总和问题:一种探索求和策略的方法

准备踏上算法解决之旅了吗?让我们探索一道经典的 LeetCode 题目——组合总和 。在这个问题中,你将面临一个整数数组 candidates 和一个目标整数 target,你的任务是找出 candidates 中所有可以相加得到 target 的组合。

探索解决方案:回溯和动态规划的交锋

解决组合总和问题有两种主要方法:回溯动态规划 。让我们逐一了解每种方法的精髓。

回溯:穷举所有可能性

回溯算法采用暴力搜索的方式,它穷举所有可能的组合,直到找到满足条件的组合。我们从数组的第一个元素开始,枚举它是否被选中,然后继续枚举第二个元素,以此类推。如果当前枚举的组合满足条件,则将其加入结果集中;如果不满足条件,则继续枚举下一个组合。

动态规划:逐步构建解决方案

动态规划是一种自底向上的方法,它通过将问题分解成更小的子问题,并存储子问题的解来解决问题。对于组合总和问题,我们可以定义一个二维数组 dp,其中 dp[i][j] 表示使用数组 candidates 的前 i 个元素,能否凑出目标值 j。我们使用以下公式来计算 dp[i][j]:

dp[i][j] = dp[i-1][j] || (candidates[i] <= j && dp[i][j-candidates[i]])

其中,dp[i-1][j] 表示使用数组 candidates 的前 i-1 个元素,能否凑出目标值 j;candidates[i] 表示数组 candidates 的第 i 个元素;dp[i][j-candidates[i]] 表示使用数组 candidates 的前 i 个元素,能否凑出目标值 j-candidates[i]。

代码实现:解谜的钥匙

掌握了算法策略后,让我们用代码来实现它们。这里有两种不同的方法的 Python 代码示例:

回溯:

def combinationSum(candidates, target):
  result = []

  def backtrack(start, current_sum, combination):
    if current_sum == target:
      result.append(combination.copy())
      return

    if current_sum > target or start == len(candidates):
      return

    combination.append(candidates[start])
    backtrack(start, current_sum + candidates[start], combination)
    combination.pop()
    backtrack(start + 1, current_sum, combination)

  backtrack(0, 0, [])
  return result

动态规划:

def combinationSum(candidates, target):
  dp = [[False] * (target + 1) for _ in range(len(candidates) + 1)]

  for i in range(1, len(candidates) + 1):
    dp[i][0] = True

  for i in range(1, len(candidates) + 1):
    for j in range(1, target + 1):
      dp[i][j] = dp[i-1][j]
      if candidates[i-1] <= j:
        dp[i][j] |= dp[i][j-candidates[i-1]]

  result = []

  def backtrack(i, current_sum, combination):
    if i == len(candidates):
      if current_sum == target:
        result.append(combination.copy())
      return

    if dp[i+1][target-current_sum]:
      backtrack(i+1, current_sum, combination)

    if candidates[i] <= target-current_sum and dp[i+1][target-current_sum-candidates[i]]:
      combination.append(candidates[i])
      backtrack(i, current_sum + candidates[i], combination)
      combination.pop()

  backtrack(0, 0, [])

  return result

常见问题解答:深入理解

  1. 回溯和动态规划有什么区别?

    • 回溯采用穷举法,而动态规划采用自底向上的方法。回溯适合解决规模较小的问题,而动态规划适合解决规模较大、存在重叠子问题的问题。
  2. 何时使用回溯,何时使用动态规划?

    • 如果问题具有重叠子问题,并且规模较大,则使用动态规划。如果问题规模较小,并且没有重叠子问题,则使用回溯。
  3. 为什么动态规划需要两个循环?

    • 外层循环用于枚举数组的元素,内层循环用于枚举目标值。
  4. 结果列表中为什么会有重复的组合?

    • 如果数组中存在重复的元素,则结果列表中也会出现重复的组合。要避免这种情况,可以在回溯或动态规划过程中对数组进行排序,然后跳过重复的元素。
  5. 如何优化组合总和问题的求解?

    • 可以使用剪枝技术来优化求解,例如在回溯时如果当前组合的和已经大于 target,则可以跳过该组合。