算法最优解 算法最优解
首页
目录
赞助
GitHub (opens new window)
首页
目录
赞助
GitHub (opens new window)
  • 数据结构基础

  • 数组

  • 位运算

  • 动态规划

    • 爬楼梯
    • 零钱兑换
    • 最长递增子序列
    • 最长公共子序列
    • 单词拆分
    • 组合总和
      • 题目描述
      • 知识回顾
      • 思路解析
      • C++代码
      • 复杂度分析
    • 打家劫舍
    • 打家劫舍 II
    • 解码方法
    • 不同路径
    • 跳跃游戏
  • 图

  • 区间

  • 链表

  • 矩阵

  • 字符串

  • 树

  • 堆

  • 逻辑思维

  • 目录
  • 动态规划
华南溜达虎
2024-07-08
目录

组合总和

题目链接: https://leetcode.cn/problems/combination-sum/

# LeetCode 39. 组合总和

# 题目描述

给你一个无重复元素的整数数组candidates和一个目标整数target,找出candidates中可以使数字和为目标数target的所有不同组合,并以列表形式返回。你可以按任意顺序返回这些组合。

candidates中的同一个数字可以无限制重复被选取。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为target的不同组合数少于150个。

举个例子:

输入: candidates = [2,3,6,7], target = 7
输出: [[2,2,3],[7]]
解释:
2和3可以形成一组候选,2 + 2 + 3 = 7。注意2可以使用多次。
7也是一个候选,7 = 7。
仅有这两种组合。

# 知识回顾

回溯是一种通过将原问题分解为子问题来求解复杂问题的算法思想。它通常用于求解组合问题、排列问题等。回溯的核心思想是通过自上而下递归的方式枚举所有可能的解,直到找到符合条件的解为止。回溯算法通常使用递归函数来实现,每次递归时都会对当前状态进行判断,如果符合条件则继续递归,否则回溯到上一层状态。

# 思路解析

本题是组合问题需要枚举所有可能的解,可以用回溯法来解,回溯的关键在于划分子问题和确定回溯条件。

下图是target = 7, candidates = [2, 3, 7],自上而下枚举所有可能解的过程,叶子节点为0,说明找到一个符合条件的解,其解就是由根节点到叶子节点之间树枝上的数组成。比如[2, 2, 3],[2, 3, 2],[3, 2, 2],[7]都是符合条件的解。

从上面的例子可以看出对于target = 7的情况可以划分为求target = 5,target = 4,target = 0三个子问题,这样就明确了子问题的划分: 对于target = n,我们可以划分成target = n - candidates[0],...,target = n - candidates[m-1],m个子问题,其中m为candidates的长度。

接下来就是确定回溯条件,很明显本题的回溯条件是叶子节点为0或为负数。

到这里回溯的两个关键问题都已经明确了,细心的同学应该已经看到对于上面的例子推导出了重复的解,[2, 2, 3],[2, 3, 2],[3, 2, 2]这三个解是重复的,这个时候就需要剪枝来剪掉重复的解。要如何剪枝呢,其实只需要在推导值为n - candidates[i]节点的子节点时,令target = n - candidates[i] - candidates[j],i <= j < m就可以将重复的解给剪掉。

有些同学对剪枝这部分有点疑惑,还是用上面的例子,target = 7,零钱的集合为candidates=[2, 3, 7]。我们可以选择兑换三个2、两个2、一个2、零个2,四种情况。每种情况我们都选择把要兑换的2一次性换完,然后再从除去2以后的零钱集合[3, 7]去兑换剩余的钱。这样就避免了重复解,如果还是有疑惑可以在纸上画一画,可以更直观的去理解。

# C++代码

class Solution {
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>> res;
        vector<int> temp;
        help(candidates, target, 0, res, temp);
        return res;
    }

    void help(vector<int>& candidates, int target, int start, vector<vector<int>>& res, vector<int>& temp) {
        //回溯条件
        if (target == 0) {
            //找到符合条件的解,保存起来
            res.push_back(temp);
            //开始回溯
            return;
        }
        //回溯条件
        if (target < 0) {
            //开始回溯
            return;
        }

        int candidates_len = candidates.size();
        //i=start是剪枝操作,如果i=0会存在重复解。
        for (int i = start; i < candidates_len; ++i) {
            if (target - candidates[i] >= 0) {
                temp.push_back(candidates[i]);
                //子问题
                help(candidates, target - candidates[i], i, res, temp);
                //回到上一个状态
                temp.pop_back();  
            }
        }

    }
};

# 复杂度分析

时间复杂度: 时间复杂度为整个推导树节点的个数,最大为 1 + m + m2 + ... + mk-1,其中m为candidates的长度,k为树的高度。

空间复杂度: 空间复杂度跟解的个数有关,为所有解的元素个数之和。

上次更新: 2024/07/28, 17:12:00
单词拆分
打家劫舍

← 单词拆分 打家劫舍→

Theme by Vdoing | Copyright © 2024-2024 华南溜达虎 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式