LeetCode 40. Combination Sum II【回溯,剪枝】中等

2023-09-14 01:03:05

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。


给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。

示例 1:

输入: candidates = `[10,1,2,7,6,1,5]`, target = `8`,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

提示:

  • 1 <= candidates.length <= 100
  • 1 <= candidates[i] <= 50
  • 1 <= target <= 30

解法 回溯+剪枝

由于我们需要求出所有和为 t a r g e t target target 的组合,并且每个数只能使用一次,因此我们可以使用递归 + 回溯的方法来解决这个问题:

我们用 d f s ( p o s , r e s t ) dfs(pos,rest) dfs(pos,rest) 表示递归的函数,其中 p o s pos pos 表示我们当前递归到了数组 c a n d i d a t e s candidates candidatess 中的第 p o s pos pos 个数,而 r e s t rest rest 表示我们还需要选择和为 r e s t rest rest 的数放入列表作为一个组合;

对于当前的第 p o s pos pos 个数,我们有两种方法:选或者不选。

  • 如果我们选了这个数,那么我们调用 d f s ( p o s + 1 , r e s t − c a n d i d a t e s [ p o s ] ) dfs(pos+1,rest−candidates[pos]) dfs(pos+1,restcandidates[pos]) 进行递归,注意这里必须满足 r e s t ≥ c a n d i d a t e s [ p o s ] rest≥candidates[pos] restcandidates[pos]
  • 如果我们不选这个数,那么我们调用 d f s ( p o s + 1 , r e s t ) dfs(pos+1,rest) dfs(pos+1,rest) 进行递归;

在某次递归开始前,如果 r e s t rest rest 的值为 0 0 0 ,说明我们找到了一个和为 t a r g e t target target 的组合,将其放入答案中。每次调用递归函数前,如果我们选了那个数,就需要将其放入列表的末尾,该列表中存储了我们选的所有数。在回溯时,如果我们选了那个数,就要将其从列表的末尾删除。

上述算法就是一个标准的递归 + 回溯算法,但是它并不适用于本题。这是因为题目描述中规定了解集不能包含重复的组合,而上述的算法中并没有去除重复的组合。例如当 c a n d i d a t e s = [ 2 , 2 ] candidates=[2,2] candidates=[2,2] t a r g e t = 2 target=2 target=2 时,上述算法会将列表 [ 2 ] [2] [2] 放入答案两次。

因此,我们需要改进上述算法,在求出组合的过程中就进行去重的操作。我们可以考虑将相同的数放在一起进行处理,也就是说,如果数 x x x 出现了 y y y 次,那么在递归时一次性地处理它们,即分别调用选择 0 , 1 , ⋯   , y 0,1,⋯ ,y 0,1,,y x x x 的递归函数。这样我们就不会得到重复的组合。具体地:

  1. 我们使用一个哈希映射(HashMap)统计数组 c a n d i d a t e s candidates candidates 中每个数出现的次数。在统计完成之后,我们将结果放入一个列表 f r e q freq freq 中,方便后续的递归使用。
  2. 列表 f r e q freq freq 的长度即为数组 c a n d i d a t e s candidates candidatess 中不同数的个数。其中的每一项对应着哈希映射中的一个键值对,即某个数以及它出现的次数。
  3. 在递归时,对于当前的第 p o s pos pos 个数,它的值为 f r e q [ p o s ] [ 0 ] freq[pos][0] freq[pos][0] ,出现的次数为 f r e q [ p o s ] [ 1 ] freq[pos][1] freq[pos][1] ,那么我们可以调用
    dfs ( pos + 1 , rest − i × freq [ pos ] [ 0 ] ) \textit{dfs}(\textit{pos} + 1, \textit{rest} - i \times \textit{freq}[\textit{pos}][0]) dfs(pos+1,resti×freq[pos][0])
    即我们选择了这个数 i i i 次。这里 i i i 不能大于这个数出现的次数,并且 i × freq [ pos ] [ 0 ] i \times \textit{freq}[\textit{pos}][0] i×freq[pos][0] 也不能大于 rest \textit{rest} rest 。同时,我们需要将 i i i freq [ pos ] [ 0 ] \textit{freq}[\textit{pos}][0] freq[pos][0] 放入列表中。

这样一来,我们就可以不重复地枚举所有的组合了。

我们还可以进行什么优化(剪枝)呢?一种比较常用的优化方法是,我们将 f r e q freq freq 根据数从小到大排序,这样我们在递归时会先选择小的数,再选择大的数(同[[LeetCode 39. Combination Sum【回溯,剪枝】中等]]的剪枝优化一样)。这样做的好处是,当我们递归到 d f s ( p o s , r e s t ) dfs(pos,rest) dfs(pos,rest) 时,如果 f r e q [ p o s ] [ 0 ] freq[pos][0] freq[pos][0] 已经大于 r e s t rest rest,那么后面还没有递归到的数也都大于 r e s t rest rest ,这就说明不可能再选择若干个和为 r e s t rest rest 的数放入列表了。此时,我们就可以直接回溯。

class Solution {
public:
    vector<pair<int, int>> freq;
    vector<vector<int>> ans;
    vector<int> seq;
    void dfs(int pos, int rest) {
        if (rest == 0) {
            ans.push_back(seq);
            return;
        }
        if (pos == freq.size() || rest < freq[pos].first) return;
        // 直接跳过
        dfs(pos + 1, rest);
        int most = min(rest / freq[pos].first, freq[pos].second);
        for (int i = 1; i <= most; ++i) {
            seq.push_back(freq[pos].first);
            dfs(pos + 1, rest - i * freq[pos].first);
        }
        for (int i = 1; i <= most; ++i) seq.pop_back();
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        for (int num : candidates) {
            if (freq.empty() || num != freq.back().first)
                freq.emplace_back(num, 1);
            else ++freq.back().second;
        }
        dfs(0, target);
        return ans;
    }
};

复杂度分析:

  • 时间复杂度: O ( 2 n × n ) O(2^n \times n) O(2n×n) ,其中 n n n 是数组 c a n d i d a t e s candidates candidates 的长度。在大部分递归 + 回溯的题目中,我们无法给出一个严格的渐进紧界,故这里只分析一个较为宽松的渐进上界。在最坏的情况下,数组中的每个数都不相同,那么列表 f r e q freq freq 的长度同样为 n n n 。在递归时,每个位置可以选或不选,如果数组中所有数的和不超过 t a r g e t target target ,那么 2 n 2^n 2n 种组合都会被枚举到;在 t a r g e t target target 小于数组中所有数的和时,我们并不能解析地算出满足题目要求的组合的数量,但我们知道每得到一个满足要求的组合,需要 O ( n ) O(n) O(n) 的时间将其放入答案中,因此我们将 O ( 2 n ) O(2^n) O(2n) O ( n ) O(n) O(n) 相乘,即可估算出一个宽松的时间复杂度上界。由于 O ( 2 n × n ) O(2^n \times n) O(2n×n) 在渐进意义下大于排序的时间复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn) ,因此后者可以忽略不计。
  • 空间复杂度: O ( n ) O(n) O(n) 。除了存储答案的数组外,我们需要 O ( n ) O(n) O(n) 的空间存储列表 f r e q freq freq 、递归中存储当前选择的数的列表、以及递归需要的栈。
更多推荐

固定资产管理措施怎么写

固定资产管理措施是指企业在进行固定资产管理时所采取的各种措施和方法。以下是一些常见的固定资产管理措施:加强固定资产的安全保护。该公司采取了多种安全措施建立完善的固定资产管理制度。制定明确的资产采购、使用、维护、报废等流程和标准,确保资产管理的规范性和透明度。采用先进的资产管理软件。通过数字化手段对固定资产进行管理和监控

unity打包后无法读取Excel解决方法

一、前言最近几乎遇到了所有能遇到的unity读取Excel的问题。因为使用的是unity5.4,而且还是32位。所以出现各种问题在所难免。废话不多说,现有的现象是:在unity的编辑器里可以完美运行,读取Excel不成问题,但是打包成exe后就无法读取到对应路径下的Excel表格了。二、解决办法第一种,未能解决:在脚本

BANI时代下,项目如何实现价值交付?

随着时代的变化,继VUCA时代后、新的语言出现:BANI一词逐渐流行起来。BANI,取自四个英文单词Brittle(脆弱的)、Anxious(焦虑的)、Nonlionear(非线性的)、Incomprehensible(费解的)首字母的大写。Brittleness(脆弱性):在BANI时代,系统和组织可能会突然、且无预

晨控CK-FR102系列与汇川AC800系列MODBUSTCP通讯手册

晨控CK-FR102系列与汇川AC800系列MODBUSTCP通讯手册晨控CK-FR102AN系列是一款基于射频识别技术的高频双通道读写器,读写器工作频率为13.56MHZ,支持对I-CODE2、I-CODESLI等符合ISO15693国际标准协议格式标签的读取。高频双通道读写器支持标准工业通讯协议ModbusTCP,

Arduino程序设计(十一)8×8 共阳极LED点阵显示(74HC595)

8×8共阳极LED点阵显示前言一、74HC595点阵模块1、74HC595介绍2、74HC595工作原理3、1088BS介绍4、74HC595点阵模块二、点阵显示实验1、点阵显示初探2、点阵显示进阶3、点阵显示高阶3.1点阵显示汉字(方法1)3.2点阵显示汉字(方法2)补充前言本文主要介绍8×8共阳极LED点阵显示实验

Python模拟登录豆瓣:轻松探索海量文化资源!

豆瓣是一个集电影、音乐、图书、活动等众多文化内容于一身的知名网站。在本文中,将介绍如何使用Python模拟登录豆瓣,以便获取更多的个性化推荐和参与社区活动。跟随下面的步骤,让我们一起来探索豆瓣的海量文化资源吧!一、安装必要的库在开始之前,需要确保已安装以下库:requests:发送HTTP请求,并处理登录和数据获取。B

libevent学习——event_base

event_base使用libevent函数之前需要分配一个或者多个event_base结构体。每个event_base结构体持有一个事件集合,可以检测以确定哪个事件是激活的。如果设置event_base使用锁,则可以安全地在多个线程中访问它。然而,其事件循环只能运行在一个线程中。如果需要用多个线程检测IO,则需要为每

【k8s】kube-proxy 工作模式

文章目录Userspace模式:iptables模式:负载均衡(LoadBalancing)LB轮询(RoundRobin):SessionAffinity:最少连接(LeastConnection):IP哈希(IPHash):SessionAffinity和IP哈希的异同自定义负载均衡器:IPVS模式:IPVS架构I

vue入门-->前后端分离&vue简介,vue入门,vue生命周期

前后端分离&vue简介vue入门vue生命周期1.前后端分离&vue简介什么是前后端分离?前后端分离,是开发模式上的前后端分离。当然还有项目架构方面的前后端分离,也就是在考虑请求并发,服务器性能,处理请求的效率等因素,从而进行前后端分离,对于初学者的理解来说不是那么友好,因此我们只讨论开发模式上的分离。先下定义:前后端

多位数按键操作(闪烁)数码管显示

/*-----------------------------------------------内容:按键加减数字,多个数码管显示------------------------------------------------*/#include<reg52.h>//包含头文件,一般情况不需要改动,头文件包含特殊功能

Postman 的使用教程(详细)

Postman使用教程1.是什么Postman是一个接口测试工具软件,可以帮助开发人员管理测试接口。官网:https://www.getpostman.com/2.安装建议通过官网下载安装,不要去那些乱七八糟的下载平台,或者留言获取官网下载地址:https://www.getpostman.com/downloads/

热文推荐