【算法训练-回溯算法 二】【子集组合问题】子集、组合、子集II、组合总和

news2025/1/16 17:49:29

废话不多说,喊一句号子鼓励自己:程序员永不失业,程序员走向架构!本篇Blog的主题是【回溯算法】,使用【数组】这个基本的数据结构来实现,这个高频题的站点是:CodeTop,筛选条件为:目标公司+最近一年+出现频率排序,由高到低的去牛客TOP101去找,只有两个地方都出现过才做这道题(CodeTop本身汇聚了LeetCode的来源),确保刷的题都是高频要面试考的题。

在这里插入图片描述

明确目标题后,附上题目链接,后期可以依据解题思路反复快速练习,题目按照题干的基本数据结构分类,且每个分类的第一篇必定是对基础数据结构的介绍

子集【MID】

元素无重不可复选。首先来看一个组合子集树
在这里插入图片描述

题干

基本的,不能包含重复的,不可复选的子集
在这里插入图片描述

解题思路

组合问题和子集问题其实是等价的,原解题思路,我们暂时不考虑如何用代码实现,先回忆一下我们的高中知识,如何手推所有子集?首先,生成元素个数为 0 的子集,即空集 [],为了方便表示,我称之为 S_0。然后,在 S_0 的基础上生成元素个数为 1 的所有子集,我称为 S_1
在这里插入图片描述
接下来,我们可以在 S_1 的基础上推导出 S_2,即元素个数为 2 的所有子集
在这里插入图片描述
为什么集合 [2] 只需要添加 3,而不添加前面的 1 呢?因为集合中的元素不用考虑顺序,[1,2,3] 中 2 后面只有 3,如果你添加了前面的 1,那么 [2,1] 会和之前已经生成的子集 [1,2] 重复,换句话说,我们通过保证元素之间的相对顺序不变来防止出现重复的子集,接着,我们可以通过 S_2 推出 S_3,实际上 S_3 中只有一个集合 [1,2,3],它是通过 [1,2] 推出的。整个推导过程就是这样一棵树在这里插入图片描述
注意这棵树的特性:如果把根节点作为第 0 层,将每个节点和根节点之间树枝上的元素作为该节点的值,那么第 n 层的所有节点就是大小为 n 的所有子集。你比如大小为 2 的子集就是这一层节点的值
在这里插入图片描述

代码实现

给出代码实现基本档案

基本数据结构数组
辅助数据结构
算法回溯算法
技巧

import java.util.*;


public class Solution {

    // 最终结果集
    private  List<List<Integer>> result = new LinkedList<>();
    // 定义路径存储集
    List<Integer> path = new LinkedList<>();
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param n int整型 the n
     * @return int整型
     */
    public List<List<Integer>> subsets(int[] nums) {
        backtrack(nums, 0);
        return result;
    }

    private void backtrack(int[] nums, int start) {
        // 1 结果添加到结果集
        result.add(new LinkedList<>(path));

        // 2 遍历寻找结果集
        for (int i = start; i < nums.length; i++) {
            // 2-1 执行选择
            path.add(nums[i]);

            // 2-2 继续向下探索,这里的start为i+1,标识下层路径从下一个元素选取
            backtrack( nums, i + 1);

            // 2-3 撤销选择
            path.remove(path.size() - 1);
        }
    }

}

在这里插入图片描述
我们使用 start 参数控制树枝的生长避免产生重复的子集,用 track 记录根节点到每个节点的路径的值,同时在前序位置把每个节点的路径值收集起来,完成回溯树的遍历就收集了所有子集

复杂度分析

这段代码是用于生成一个整数数组的所有子集的回溯算法。算法通过递归的方式生成子集,每次在递归中有两种选择:包含当前元素或不包含当前元素。以下是对代码的分析:

  • subsets 方法是公共的入口点,它接受一个整数数组 nums 作为输入,并返回一个包含所有子集的列表。

  • subsets 方法中,创建了一个空的 path 列表用于存储当前子集,然后调用 backtrack 方法来开始回溯过程。

  • backtrack 方法是递归函数,用于生成子集。它采用以下步骤:

    1. 将当前 path 添加到最终结果集 result 中,将当前 path 中的元素加入结果集表示一个子集。

    2. 遍历 nums 数组,从 start 开始,对于每个元素,执行以下操作:

      • 将当前元素添加到 path 中,表示选择当前元素。

      • 递归调用 backtrack 方法,但这次的 starti + 1 开始,这样可以确保在同一个子集中不重复使用元素。

      • 撤销选择,将刚刚添加的元素从 path 中移除,继续遍历下一个元素。

  • 回溯算法会递归生成所有可能的子集,直到遍历完 nums 数组中的所有元素。

  • 最终,subsets 方法返回包含所有子集的 result 列表。

时间复杂度

  • 时间复杂度主要取决于递归调用的次数。对于每个元素,有两种选择,包括或不包括,而每种状态都需要 O (n) 的构造时间,因此时间复杂度是 O(2^n),其中 n 是输入数组 nums 的大小。

空间复杂度

  • 空间复杂度主要取决于递归调用的深度和存储中间结果的数据结构。递归的深度最多为 n,因此空间复杂度是 O(n)

  • 此外,path 列表的空间复杂度也需要考虑。在最坏的情况下,path 列表的长度可能等于 n,因为每个元素都可能被包括在一个子集中。因此,总的空间复杂度为 O(n)

总结:这段代码使用回溯算法生成了输入数组 nums 的所有子集,时间复杂度为 O(2^n),空间复杂度为 O(n)

组合【MID】

元素无重不可复选。给你输入一个数组 nums = [1,2…,n] 和一个正整数 k,请你生成所有大小为 k 的子集。

题干

在这里插入图片描述

解题思路

还是以 nums = [1,2,3] 为例,刚才让你求所有子集,就是把所有节点的值都收集起来;现在你只需要把第 2 层(根节点视为第 0 层)的节点收集起来,就是大小为 2 的所有组合
在这里插入图片描述

代码实现

给出代码实现基本档案

基本数据结构数组
辅助数据结构
算法回溯算法
技巧

import java.util.*;


public class Solution {

    // 最终结果集
    private  List<List<Integer>> result = new LinkedList<>();
    // 定义路径存储集
    List<Integer> path = new LinkedList<>();
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param n int整型 the n
     * @return int整型
     */
    public List<List<Integer>> combine(int n, int k) {

        // 2 初始值进行回溯
        backtrack(n, 1, k);
        return result;
    }

    private void backtrack( int n, int start, int k) {
        // 1 结果添加到结果集
        if (path.size() == k) {
            result.add(new LinkedList<>(path));
            return;
        }

        // 2 遍历寻找结果集
        for (int i = start; i <= n; i++) {
            // 2-1 执行选择
            path.add(i);

            // 2-2 继续向下探索,这里的start为i+1,标识下层路径从下一个元素选取
            backtrack(n, i + 1, k);

            // 2-3 撤销选择
            path.remove(path.size() - 1);
        }
    }

}

复杂度分析

时间复杂度和空间复杂度同上

子集II

元素有重不可复选。 再简单补充下重复元素的情况
在这里插入图片描述

解题思路

就以 nums = [1,2,2] 为例,为了区别两个 2 是不同元素,后面我们写作 nums = [1,2,2’]。按照之前的思路画出子集的树形结构,显然,两条值相同的相邻树枝会产生重复
在这里插入图片描述
其结果为:

[ 
    [],
    [1],[2],[2'],
    [1,2],[1,2'],[2,2'],
    [1,2,2']
]

你可以看到,[2] 和 [1,2] 这两个结果出现了重复,所以我们需要进行剪枝,如果一个节点有多条值相同的树枝相邻,则只遍历第一条,剩下的都剪掉,不要去遍历
在这里插入图片描述
体现在代码上,需要先进行排序,让相同的元素靠在一起,如果发现 nums[i] == nums[i-1],则跳过,和全排列II的思路一致

代码实现

给出代码实现基本档案

基本数据结构数组
辅助数据结构
算法回溯算法
技巧

import java.util.*;


public class Solution {

    // 最终结果集
    private  List<List<Integer>> result = new LinkedList<>();
    // 定义路径存储集
    List<Integer> path = new LinkedList<>();
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param n int整型 the n
     * @return int整型
     */
    public List<List<Integer>> subsetsWithDup(int[] nums) {

        // 1 数组排序,相同值元素相邻
        Arrays.sort(nums);
        //2 初始值进行回溯
        backtrack( nums, 0);
        return result;
    }

    private void backtrack( int[] nums, int start) {
        // 1 结果添加到结果集
        result.add(new LinkedList<>(path));

        // 2 遍历寻找结果集
        for (int i = start; i < nums.length; i++) {
            // 2-1 剪枝,前面出现过的元素不再遍历
            if (i > start && nums[i] == nums[i - 1]) {
                continue;
            }
            // 2-2 执行选择
            path.add(nums[i]);

            // 2-3 继续向下探索,这里的start为i+1,标识下层路径从下一个元素选取
            backtrack(nums, i + 1);

            // 2-4 撤销选择
            path.remove(path.size() - 1);
        }
    }

}

复杂度分析

时间和空间复杂度同上

组合总和

元素无重可复选。这道题目也比较高频
在这里插入图片描述

题干

在这里插入图片描述

解题思路

这道题说是组合问题,实际上也是子集问题:candidates 的哪些子集的和为 target,想解决这种类型的问题,也得回到回溯树上,我们不妨先思考思考,标准的子集/组合问题是如何保证不重复使用元素的?答案在于 backtrack 递归时输入的参数 start

// 无重组合的回溯算法框架
void backtrack(int[] nums, int start) {
    for (int i = start; i < nums.length; i++) {
        // ...
        // 递归遍历下一层回溯树,注意参数
        backtrack(nums, i + 1);
        // ...
    }
}

这个 i 从 start 开始,那么下一层回溯树就是从 start + 1 开始,从而保证 nums[start] 这个元素不会被重复使用:
在这里插入图片描述
那么反过来,如果我想让每个元素被重复使用,我只要把 i + 1 改成 i 即可

// 可重组合的回溯算法框架
void backtrack(int[] nums, int start) {
    for (int i = start; i < nums.length; i++) {
        // ...
        // 递归遍历下一层回溯树,注意参数
        backtrack(nums, i);
        // ...
    }
}

这相当于给之前的回溯树添加了一条树枝,在遍历这棵树的过程中,一个元素可以被无限次使用
在这里插入图片描述
当然,这样这棵回溯树会永远生长下去,所以我们的递归函数需要设置合适的 base case 以结束算法,即路径和大于 target 时就没必要再遍历下去了

代码实现

给出代码实现基本档案

基本数据结构数组
辅助数据结构
算法回溯算法
技巧

import java.util.*;


public class Solution {

    // 最终结果集
    private  List<List<Integer>> result = new LinkedList<>();
    // 定义路径存储集
    List<Integer> path = new LinkedList<>();
    // 定义随路径存储变化的总和
    int pathSum = 0;

    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param n int整型 the n
     * @return int整型
     */

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates.length == 0) {
            return new LinkedList<>();
        }
        backtrack( candidates, 0, target);
        return result;
    }

    private void backtrack( int[] nums, int start, int target) {
        // 1 等于目标和结果添加到结果集
        if (pathSum == target) {
            result.add(new LinkedList<>(path));
            return;
        }

        // 2 超过目标和返回
        if (pathSum > target) {
            return;
        }

        // 3 遍历寻找结果集
        for (int i = start; i < nums.length; i++) {
            // 3-1 执行选择
            path.add(nums[i]);
            pathSum += nums[i];

            // 3-2 继续向下探索,这里的start为i表示元素可以重复被使用
            backtrack(nums, i, target);

            // 3-3 撤销选择
            path.remove(path.size() - 1);
            pathSum -= nums[i];

        }
    }

}

复杂度分析

时间和空间复杂度同上

拓展知识:组合子集问题

无论是排列、组合还是子集问题,简单说无非就是让你从序列 nums 中以给定规则取若干元素,主要有以下几种变体:

  • 形式一、元素无重不可复选,即 nums 中的元素都是唯一的,每个元素最多只能被使用一次,这也是最基本的形式。以组合为例,如果输入 nums = [2,3,6,7],和为 7 的组合应该只有 [7]。全排列、子集、组合
  • 形式二、元素可重不可复选,即 nums 中的元素可以存在重复,每个元素最多只能被使用一次。以组合为例,如果输入 nums = [2,5,2,1,2],和为 7 的组合应该有两种 [2,2,2,1] 和 [5,2]。全排列II、子集II
  • 形式三、元素无重可复选,即 nums 中的元素都是唯一的,每个元素可以被使用若干次。以组合为例,如果输入 nums = [2,3,6,7],和为 7 的组合应该有两种 [2,2,3] 和 [7]。组合总和

当然,也可以说有第四种形式,即元素可重可复选。但既然元素可复选,那又何必存在重复元素呢?元素去重之后就等同于形式三,所以这种情况不用考虑。上面用组合问题举的例子,但排列、组合、子集问题都可以有这三种基本形式,所以共有 9 种变化

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1107921.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【APP源码】基于Typecho博客程序开发的博客社区资讯APP源码

全新博客社区资讯APP源码 Typecho后端 一款功能全面&#xff0c;用户交互良好&#xff0c;数据本地缓存&#xff0c;集成邮箱验证&#xff0c;在线投稿&#xff0c;&#xff08;内置Mardown编辑器&#xff09;&#xff0c; 快捷评论的的博客资讯APP。同时兼容H5和微信小程序。 …

基于nodejs+vue学生论坛设计与实现

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

python二次开发CATIA:CATIA Automation

CATIA 软件中有一套逻辑与关系都十分严谨的自动化对象&#xff0c;它们从CATIA(Application)向下分支。每个自动化对象&#xff08;Automation Object&#xff0c;以下简称Object&#xff09;都有各自的属性与方法。我们通过程序语言调用这些 Object 的属性与方法&#xff0c;便…

spring 资源操作:Resources

文章目录 Spring Resources概述Resource接口Resource的实现类UrlResource访问网络资源ClassPathResource 访问类路径下资源FileSystemResource 访问文件系统资源ServletContextResourceInputStreamResourceByteArrayResource Resource类图ResourceLoader 接口ResourceLoader 概…

【数之道 08】走进“卷积神经网络“,了解图像识别背后的原理

卷积神经网络 CNN模型的架构Cnn 的流程第一步 提取图片特征提取特征的计算规则 第二步 最大池化第三步 扁平化处理第四步 数据条录入全连接隐藏层 b站视频 CNN模型的架构 图片由像素点组成&#xff0c;最终成像效果由背后像素的颜色数值所决定的 有这样的一个66的区域&#x…

数字货币和区块链:跨境电商的未来之革命

随着全球数字化浪潮的不断涌现&#xff0c;跨境电商正经历着前所未有的革命。其中&#xff0c;数字货币和区块链技术被认为是这场革命的关键驱动力。 它们不仅改变了支付方式&#xff0c;还提供了更安全、高效的交易体验&#xff0c;同时也为跨境电商开启了新的商业模式和机会…

nodejs基于vue 学生论坛设计与实现

随着网络技术的不断成熟&#xff0c;带动了学生论坛&#xff0c;它彻底改变了过去传统的管理方式&#xff0c;不仅使服务管理难度变低了&#xff0c;还提升了管理的灵活性。 是本系统的开发平台 系统中管理员主要是为了安全有效地存储和管理各类信息&#xff0c; 这种个性化的平…

static关键字总结-C/C++

引言&#xff1a;由于怕忘记static的一些区别&#xff0c;今天来写一篇文章尽可能的覆盖到static在C/C中的用法和易错点。 第一部分 C中的static 1. static修饰变量 被修饰的变量只能被定义一次&#xff0c;如下代码&#xff0c;n经过循环后仍然还是10。 #include <stdio…

springMVC中统一异常处理@ControllerAdvice

1.在DispatcherServlet中初始化HandlerExceptionResolver 2.controller执行完成后执行processDispatchResult(processedRequest,response,mappedHandler,mv,dispatchException),有异常则处理异常 3.ExcepitonHandlerExceptionResolver中执行方法doResolveHandlerMethodExceptio…

数据结构复盘——第八章:排序

文章目录 第一部分&#xff1a;各种排序方法的比较第二部分&#xff1a;插入排序1、直接插入排序2、折半插入排序3、希尔排序 第三部分&#xff1a;交换排序1、冒泡排序2、快速排序 第四部分&#xff1a;选择排序1、简单选择排序2、堆排序2.1 堆的概念2.2 堆的调整算法2.3 堆的…

计算机网络-计算机网络体系结构-网络层

目录 一、IPV4 IP数据报格式 *IP 数据报分片 *IPV4地址 分类 网络地址转换(NAT) 二、子网划分与子网掩码 *CIDR *超网 协议 ARP协议 DHCP协议 ICMP协议 三、IPV6 格式 IPV4和IPV6区别 地址表示形式 四、路由选择协议 RIP(路由信息协议) OPSF(开发最短路径优…

为什么高精度机器人普遍使用谐波减速器而不是普通减速器?

机器人作为一种能够代替人类完成各种工作的智能设备&#xff0c;已经广泛应用于工业生产、医疗卫生、军事防卫等领域。其中&#xff0c;机器人的关节传动系统是机器人运动的核心&#xff0c;而减速器作为关节传动系统中的重要组成部分部分&#xff0c;对机器人的性能和技术水平…

volatile-两大特性(可见性、有序性)、内存屏障

6.1 被volatile修饰的变量有两大特点 ● 特点&#xff1a;○ 可见性○ 有序性&#xff1a;有排序要求&#xff0c;有时需要禁重排● 内存语义&#xff1a;○ 当写一个volatile变量时&#xff0c;JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中○ 当读一个vola…

Zabbix“专家坐诊”第207期问答汇总

问题一 Q&#xff1a;不小心把host表删除了&#xff0c;怎么处理&#xff1f;现在使用的zabbix 4.0.3的server&#xff0c;agent是4.2.1&#xff0c;能不能不动agent的情况下升级server版本&#xff0c;重新部署&#xff1f; A&#xff1a;数据库有备份话恢复即可&#xff0c;…

当下流行的编程语言

在任何时候&#xff0c;一些编程语言都会把大量的开发人员变成热情的布道者&#xff0c;试图说服世界其他地方的人相信它的伟大。 当热起来的时候&#xff0c;这种语言可能会成为行业标准&#xff0c;但其他时候&#xff0c;这种受欢迎程度就会消失。 1、数据的由来 每年Stack…

关于vue2回显表格数据忽略中间空格补全

关于vue2回显表格数据忽略中间空格补全 发现问题解决 发现问题 发现回显数据中间空格忽略 解决 在全局中修改在页面内修改 主要案例主要是在页面内修改 ::v-deep .cell{white-space: pre; }

COMSOL超声换能器聚焦声场仿真

超声聚焦 超声聚焦广泛应用于各类工业设备与技术中&#xff0c;例如我们熟悉的无损检测&#xff08;NDT&#xff09;和医学成像。高强度聚焦超声&#xff08;HIFU&#xff09;是此技术的一项临床应用&#xff0c;它利用探头将大部分能量集中到目标组织区域&#xff0c;使组织发…

uniapp实现简单的九宫格抽奖(附源码)

效果展示 uniapp实现大转盘抽奖 实现步骤&#xff1a; 1.该页面可设置8个奖品&#xff0c;每个奖品可设置中奖机会的权重&#xff0c;如下chance越大&#xff0c;中奖概率越高&#xff08;大于0&#xff09; // 示例代码 prizeList: [{id: 1,image: "https://img.alicdn…

【好书推荐】深入理解现代JavaScript

作者介绍 T. J. Crowder是一位拥有30年经验的软件工程师。在他的整个职业生涯中&#xff0c;他至少有一半时间是在使用JavaScript从事开发工作。他经营着软件承包和产品公司Farsight Software。他经常在Stack Overflow上为人们提供帮助&#xff0c;他是十大贡献者之一和JavaScr…

Java身份证实名认证-阿里云API 【姓名、身份证号】

1. 阿里云API市场 https://market.aliyun.com/products/57126001/cmapi00053442.html?spm5176.2020520132.101.3.a6217218nxxEiy#skuyuncode47442000022 购买对应套餐 2. 复制AppCode https://market.console.aliyun.com/imageconsole/index.htm#/?_kl85e10 云市场-已购买服…