77. 组合【含回溯详解、N叉树类比、剪枝优化】

news2024/12/22 16:24:42

文章目录

  • 77. 组合
  • 思路
    • 暴力法
    • 回溯与N叉树类比
    • 回溯法三部曲
  • 总结
  • 剪枝优化
  • 剪枝总结

77. 组合

77. 组合

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

输入:n = 1, k = 1
输出:[[1]]

提示:

  • 1 <= n <= 20
  • 1 <= k <= n

思路

暴力法

本题是回溯法的经典题目。

直接的解法当然是使用for循环,例如示例中k2,很容易想到用两个for循环,这样就可以输出和示例中一样的结果。

代码如下:

n := 4;
for i := 1; i <= n; i++ {
    for j := i + 1; j <= n; j++ {
        fmt.Printf("%d %d\n",i,j)
    }
}

输入:n = 100, k = 3 那么就三层for循环,代码如下:

n := 100;
for  i := 1; i <= n; i++ {
    for j := i + 1; j <= n; j++ {
        for  u := j + 1; u <= n; n++ {
       	 fmt.Printf("%d %d %d\n",i,j,u)
        }
    }
}

如果n100k50呢,那就50for循环,是不是开始窒息。

此时就会发现虽然想暴力搜索,但是用for循环嵌套连暴力都写不出来!

咋整?

回溯搜索法来了,虽然回溯法也是暴力,但至少能写出来,不像for循环嵌套k层让人绝望。

那么回溯法怎么暴力搜呢?

上面我们说了要解决 n100k50的情况,暴力写法需要嵌套50for循环,那么回溯法就用递归来解决嵌套层数的问题。

递归来做层叠嵌套(可以理解是开k层for循环),每一次的递归中嵌套一个for循环,那么递归就可以用于解决多层嵌套循环的问题了。

此时递归的层数大家应该知道了,例如:n100k50的情况下,就是递归50层。

一些同学本来对递归就懵,回溯法中递归还要嵌套for循环,可能就直接晕倒了!

如果脑洞模拟回溯搜索的过程,绝对可以让人窒息,所以需要抽象图形结构来进一步理解。

回溯与N叉树类比

我们在关于回溯算法基础中说到回溯法解决的问题都可以抽象为树形结构(N叉树),用树形结构来理解回溯就容易多了。

那么我把组合问题抽象为如下树形结构:

在这里插入图片描述

可以看出这棵树,一开始集合是 1,2,3,4, 从左向右取数,取过的数,不再重复取。

第一次取1,集合变为2,3,4 ,因为k2,我们只需要再取一个数就可以了,分别取2,3,4,得到集合[1,2] [1,3] [1,4],以此类推。

每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。

图中可以发现n相当于树的宽度,k相当于树的深度。

那么如何在这个树上遍历,然后收集到我们要的结果集呢?

图中每次搜索到了叶子节点,我们就找到了一个结果。

相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合。

如果用N叉树前序遍历类比一下或许就更好理解了。如下是N叉树前序遍历的代码,可以看到每遍历到一个节点后,就递归遍历它所有的子孩子,同样,它子孩子也可能还有子孩子,且各子孩子的子孩子数量可以是不一致的。

N 叉树前序遍历

/**
 * Definition for a Node.
 * type Node struct {
 *     Val int
 *     Children []*Node
 * }
 */

func preorder(root *Node) []int {
    if root == nil {
        return nil
    }

    res := make([]int,0)
    dfs(root,&res)

    return res
}

func dfs(root *Node,res *[]int) {
    // 递归终止条件
    if root == nil {
        return 
    }
    
    *res = append(*res,root.Val)
    // 递归前序遍历当前节点的每一个子节点
    for i := 0;i < len(root.Children);i++ {
        dfs(root.Children[i],res)
    }
}

这时候再来看下图
在这里插入图片描述
是不是可以理解成根节点有四个子孩子(当然,这里根节点在上图没有体现,可以想象图中第一层是根节点的所有子孩子),分别是1,2,3,4,我们递归遍历孩子1,然后发现1又有三个子孩子2,3,4,继续递归遍历1的这三个子孩子,都遍历完后上去,到达第一层,此时再开始遍历根节点的第二个孩子2,同样发现2也有两个子孩子3,4,所以遍历34,依次递推,直到全部节点遍历结束。

小结: 回溯可以类比成一个N叉树,每一层的for就是横向选择一个子孩子,纵向就是递归遍历该子孩子的子孩子,可以将组合题目的代码和N叉树的递归代码对比看下,更有感觉

回溯法三部曲

1.递归函数的返回值以及参数
函数里一定有两个参数,既然是集合n里面取k个数,那么nk是两个int型的参数。

在这里还需要两个切片,一个用来存放符合条件的单一结果path,一个用来存放符合条件结果的集合res

然后还需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。

为什么要有这个startIndex呢?

startIndex 就是防止出现重复的组合。

从下图中红线部分可以看出,在集合[1,2,3,4]1之后,下一层递归,就要在[2,3,4]中取数了,那么下一层递归如何知道从[2,3,4]中取数呢,靠的就是startIndex
在这里插入图片描述

所以需要startIndex来记录下一层递归时,搜索的起始位置。

那么backtracing函数定义整体代码如下:

注意res和path都是切片的指针类型,因为在递归的时候,他们两个都可能产生扩容,改变指向,所以需要传指针,保证操作的是我们最开始传入的切片。

func backtracing(n int,k int,res *[][]int,path *[]int,startIndex int) {}

2.回溯函数终止条件
什么时候到达所谓的叶子节点了呢?

path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。

如图红色部分:

在这里插入图片描述

此时用res二维切片,把path保存起来,并终止本层递归。

所以终止条件代码如下:

if len(*path) == k {
    // 需要添加的是path的副本,注意如下添加副本技巧
     *res = append(*res,append([]int(nil),*path...))
     return
 }

3.单层搜索的过程
回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。

在这里插入图片描述

如此我们才遍历完图中的这棵树。

for循环每次从startIndex开始遍历,然后用path保存取到的节点i

代码如下:

// 控制树的横向遍历
for i := startIndex;i <= n;i++ {
    *path = append(*path,i) // 处理节点
    // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
    backtracing(n,k,res,path,i + 1)
    *path = (*path)[0:len(*path) - 1]  // 回溯,撤销处理的节点
}

可以看出backtracking(递归函数)通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回。

backtracking的下面部分就是回溯的操作了,撤销本次处理的结果。

关键地方都讲完了,组合问题Go完整代码如下:

func combine(n int, k int) [][]int {
    res := make([][]int,0)
    path := make([]int,0)
    backtracing(n,k,&res,&path,1)
    return res
}

func backtracing(n int,k int,res *[][]int,path *[]int,startIndex int) {
    if len(*path) == k {
        // 需要添加的是path的副本,注意如下添加副本技巧
        *res = append(*res,append([]int(nil),*path...))
        return
    }
    for i := startIndex;i <= n;i++ {
        *path = append(*path,i)
        backtracing(n,k,res,path,i + 1)
        *path = (*path)[0:len(*path) - 1] // 回溯,撤销结果
    }
}

时间复杂度: O ( n ∗ 2 n ) O(n * 2^n) O(n2n)
空间复杂度: O ( n ) O(n) O(n)
在这里插入图片描述

还记得我们在回溯算法理论基础中给出的回溯法模板么?

如下:

func backtracking(参数) {
    if 终止条件 {
        存放结果
        return
    }

    for 选择:本层集合中元素(树中节点孩子的数量就是集合的大小) {
        处理节点;
        backtracking(路径,下一层的选择列表) // 递归
        回溯,撤销处理结果
    }
}

对比一下本题的代码,是不是发现有点像! 所以有了这个模板,就有解题的大体方向,不至于毫无头绪。

总结

组合问题是回溯法解决的经典问题,我们开始的时候给大家列举一个很形象的例子,就是n100k50的话,直接想法就需要50for循环。

从而引出了回溯法就是解决这种kfor循环嵌套的问题。

然后进一步把回溯法的搜索过程抽象为树形结构,可以直观的看出搜索的过程。

接着用回溯法三部曲,逐步分析了函数参数、终止条件和单层搜索的过程。

剪枝优化

我们说过,回溯法虽然是暴力搜索,但也有时候可以有点剪枝优化一下的。

在遍历的过程中有如下代码:

for i := startIndex;i <= n;i++ {
    *path = append(*path,i)
    backtracing(n,k,res,path,i + 1)
    *path = (*path)[0:len(*path) - 1] 
}

这个遍历的范围是可以剪枝优化的,怎么优化呢?

来举一个例子,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了。

这么说有点抽象,如图所示:
在这里插入图片描述

图中每一个节点(图中为矩形),就代表本层的一轮for循环,那么每一层的for循环从第二个数开始遍历的话,都没有意义,都是无效遍历,因为剩下的全部数都选上,最终也达不到4个数了。

所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。

如果for循环选择的起始位置之后的元素个数 【已经不足】 我们需要的元素个数了,那么就没有必要搜索了。

注意代码中i,就是for循环里选择的起始位置。

for i := startIndex; i <= n; i++ {}

接下来看一下优化过程如下:

已经选择的元素个数:len(*path)

还需要的元素个数为: k - len(*path)

因此在集合n中至多可以从该起始位置 : n - (k - path.size()) + 1,开始遍历,起点从这之后开始遍历的,即使取完剩下的所有数,个数也是不足的。

为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。

举个例子,n = 4,k = 3, 目前已经选取的元素为0(len(*path)为0)n - (k - 0) + 1 4 - ( 3 - 0) + 1 = 2

2开始搜索都是合理的,可以是组合[2, 3, 4]

这里大家想不懂的话,建议也举一个例子,就知道是不是要+1了。

实际也可以是这个公式推导理解:n - i + 1 < k - len(*path),等式右边表示还需要选几个数,左边还可以选的数字数量。移位后即变为:i > n + 1 - (k - len(*path))时,数量肯定不足,可以剪枝。

所以优化之后的for循环是:

// i为本次搜索的起始位置
for i := startIndex;i <= n - (k - len(*path)) + 1;i++ {}

优化后整体代码如下:

func combine(n int, k int) [][]int {
    res := make([][]int,0)
    path := make([]int,0)
    backtracing(n,k,&res,&path,1)
    return res
}

func backtracing(n int,k int,res *[][]int,path *[]int,startIndex int) {
    if len(*path) == k {
        // 需要添加的是path的副本,注意如下添加副本技巧
        *res = append(*res,append([]int(nil),*path...))
        return
    }
    for i := startIndex;i <= n - (k - len(*path)) + 1;i++ {
        *path = append(*path,i)
        backtracing(n,k,res,path,i + 1)
        *path = (*path)[0:len(*path) - 1] // 回溯,撤销结果
    }
}

在这里插入图片描述

剪枝总结

本篇我们对求组合问题的回溯法代码做了剪枝优化,这个优化如果不画图的话,其实不好理解,也不好讲清楚。

所以我依然是把整个回溯过程抽象为一棵树形结构,然后可以直观的看出,剪枝究竟是剪的哪里。

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

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

相关文章

spring loCDI 详解

文章目录 一、IoC & DI 基本知识1.1 IoC 的基本概念&#xff1a;1.2 IoC 的优势&#xff1a;1.3 DI 介绍&#xff1a; 二、IoC 详解2.1 Spring 容器&#xff1a;2.2 被存储 Bean 的命名约定&#xff1a;2.3 Bean 的存储方式&#xff1a;2.3.1 五大类注解&#xff1a;2.3.1.…

MySQL高阶2010-职员招聘人数2

目录 题目 准备数据 分析数据 总结 题目 一家公司想雇佣新员工。公司的工资预算是 $70000 。公司的招聘标准是&#xff1a; 继续雇佣薪水最低的高级职员&#xff0c;直到你不能再雇佣更多的高级职员。用剩下的预算雇佣薪水最低的初级职员。继续以最低的工资雇佣初级职员&…

linux文件编程_进程

1. 进程相关概念 面试中关于进程&#xff0c;应该会问的的几个问题&#xff1a; 1.1. 什么是程序&#xff0c;什么是进程&#xff0c;有什么区别&#xff1f; 程序是静态的概念&#xff0c;比如&#xff1a; 磁盘中生成的a.out文件&#xff0c;就叫做&#xff1a;程序进程是…

Linux常用语法

Linux常用语法 0.引言特殊路径符Linux 命令基础格式重要命令mkdir命令echo-tail命令 vi\vim编辑器的三种工作模式vi/vim简单介绍基础命令 运行模式命令模式下的快捷键 进程管理进程的命令 Linux解压缩tar格式zip命令unzip命令 ping,wget,curl等命令的使用Linux端口端口端口的划…

【算法篇】回溯算法类(1)(笔记)

目录 一、理论基础 1. 相关题目 2. 遍历过程 3. 代码框架 二、LeetCode 题目 1. 组合 2. 组合总和III 3. 电话号码的字母组合 4. 组合总和 5. 组合总和II 6. 分割回文串 7. 复原IP地址 8. 子集 一、理论基础 1. 相关题目 2. 遍历过程 3. 代码框架 void backtr…

光通信——APON/EPON/GPON/10G PON

目录 APON EPON GPON 上下行对称和非对称速率 OAM功能 汇聚子层 ATM封装方式 GEM封装方式 10G EPON EPON/GPON技术原理和特点 工作原理 关键技术 &#xff08;1&#xff09;测距、同步 &#xff08;2&#xff09;突发发送和接收 &#xff08;3&#xff09…

基于Word2Vec和LSTM实现微博评论情感分析

关于深度实战社区 我们是一个深度学习领域的独立工作室。团队成员有&#xff1a;中科大硕士、纽约大学硕士、浙江大学硕士、华东理工博士等&#xff0c;曾在腾讯、百度、德勤等担任算法工程师/产品经理。全网20多万粉丝&#xff0c;拥有2篇国家级人工智能发明专利。 社区特色…

【mmengine】优化器封装(OptimWrapper)(入门)优化器封装 vs 优化器

MMEngine 实现了优化器封装&#xff0c;为用户提供了统一的优化器访问接口。优化器封装支持不同的训练策略&#xff0c;包括混合精度训练、梯度累加和梯度截断。用户可以根据需求选择合适的训练策略。优化器封装还定义了一套标准的参数更新流程&#xff0c;用户可以基于这一套流…

SWAP、AquaCrop、FVCOM、Delft3D、SWAT、R+VIC、HSPF、HEC-HMS......

全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型&#xff0c;它综合考虑了土壤-水分-大气以及植被间的相互作用&#xff1b;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程&…

2024最新软件测试八股文(含答案+文档)

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、软件测试基础面试题 1、阐述软件生命周期都有哪些阶段? 常见的软件生命周期模型有哪些? 软件生命周期是指一个计算机软件从功能确定设计&#xff0c;到…

系统安全 - Linux 安全模型及实践

文章目录 导图Linux 安全模型用户层权限管理的细节多用户环境中的权限管理文件权限与目录权限 最小权限原则的应用Linux 系统中的认证、授权和审计机制认证机制授权机制审计机制 主机入侵检测系统&#xff08;HIDS&#xff09;_ Host-based Intrusion Detection SystemHIDS 的概…

Android问题笔记五十:构建错误-AAPT2 aapt2-7.0.2-7396180-windows Daemon

Unity3D特效百例案例项目实战源码Android-Unity实战问题汇总游戏脚本-辅助自动化Android控件全解手册再战Android系列Scratch编程案例软考全系列Unity3D学习专栏蓝桥系列ChatGPT和AIGC &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分…

jmeter中token测试

案例&#xff1a; 网站&#xff1a;http://shop.duoceshi.com 讲解&#xff1a;用三个接口来讲解 第一个接口code&#xff1a;GET http://manage.duoceshi.com/auth/code 第二个登录接口&#xff1a;http://manage.duoceshi.com/auth/login 第三个接口&#xff1a;http://…

iOS中的链表 - 双向链表

iOS中的链表 - 单向链表_ios 链表怎么实现-CSDN博客​​​​​​​ 引言 在数据结构中&#xff0c;链表是一种常见的且灵活的线性存储方式。与数组不同&#xff0c;链表的元素在内存中不必连续存储&#xff0c;这使得它们在动态内存分配时更加高效。其中&#xff0c;双向链表…

Pikachu-Cross-Site Scripting-DOM型xss_x

查看代码&#xff0c;输入的内容&#xff0c;通过get请求方式&#xff0c;用text 参数带过去&#xff1b; 获取text内容&#xff0c;赋值给xss 然后拼接到 dom 里&#xff1b;构造payload的关键语句&#xff1a; <a href"xss">就让往事都随风,都随风吧</a&…

【SQL】DDL语句

文章目录 1.SQL通用语法2.SQL的分类3.DDL3.1数据库操作3.2 表操作3.2.1 表操作--数据类型3.2.2 表操作--修改3.2.3 表操作--删除 SQL 全称 Structured Query Language&#xff0c;结构化查询语言。操作关系型数据库的编程语言&#xff0c;定义了一套操作关系型数据库统一标准 。…

【Python语言初识(六)】

一、网络编程入门 1.1、TCP/IP模型 实现网络通信的基础是网络通信协议&#xff0c;这些协议通常是由互联网工程任务组 &#xff08;IETF&#xff09;制定的。所谓“协议”就是通信计算机双方必须共同遵从的一组约定&#xff0c;例如怎样建立连接、怎样互相识别等&#xff0c;…

解决MySQL报Incorrect datetime value错误

目录 一、前言二、问题分析三、解决方法 一、前言 欢迎大家来到权权的博客~欢迎大家对我的博客进行指导&#xff0c;有什么不对的地方&#xff0c;我会及时改进哦~ 博客主页链接点这里–>&#xff1a;权权的博客主页链接 二、问题分析 这个错误通常出现在尝试将一个不…

基于C++和Python的进程线程CPU使用率监控工具

文章目录 0. 概述1. 数据可视化示例2. 设计思路2.1 系统架构2.2 设计优势 3. 流程图3.1 C录制程序3.2 Python解析脚本 4. 数据结构说明4.1 CpuUsageData 结构体 5. C录制代码解析5.1 主要模块5.2 关键函数5.2.1 CpuUsageMonitor::Run()5.2.2 CpuUsageMonitor::ComputeCpuUsage(…

Python库matplotlib之五

Python库matplotlib之五 小部件(widget)RangeSlider构造器APIs应用实列 TextBox构造器APIs应用实列 小部件(widget) 小部件(widget)可与任何GUI后端一起工作。所有这些小部件都要求预定义一个Axes实例&#xff0c;并将其作为第一个参数传递。 Matplotlib不会试图布局这些小部件…