组合[中等]

一、题目

给定两个整数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

二、代码

【1】递归实现组合型枚举:n个当中选k个的所有方案对应的枚举是组合型枚举。在「方法一」中我们用递归来实现组合型枚举。首先我们先回忆一下如何用递归实现二进制枚举(子集枚举),假设我们需要找到一个长度为n的序列a的所有子序列,代码框架是这样的:

vector<int> temp;
void dfs(int cur, int n) {if (cur == n + 1) {// 记录答案// ...return;}// 考虑选择当前位置temp.push_back(cur);dfs(cur + 1, n, k);temp.pop_back();// 考虑不选择当前位置dfs(cur + 1, n, k);
}

上面的代码中,dfs(cur,n)参数表示当前位置是cur,原序列总长度为n。原序列的每个位置在答案序列种的状态有被选中和不被选中两种,我们用temp数组存放已经被选出的数字。在进入dfs(cur,n)之前[1,cur−1]位置的状态是确定的,而[cur,n]内位置的状态是不确定的,dfs(cur,n)需要确定cur位置的状态,然后求解子问题dfs(cur+1,n)。对于cur位置,我们需要考虑a[cur]取或者不取,如果取,我们需要把a[cur]放入一个临时的答案数组中(即上面代码中的temp),再执行dfs(cur+1,n),执行结束后需要对temp进行回溯;如果不取,则直接执行dfs(cur+1,n)。在整个递归调用的过程中,cur是从小到大递增的,当cur增加到n+1的时候,记录答案并终止递归。可以看出二进制枚举的时间复杂度是O(2^n)

组合枚举的代码框架可以借鉴二进制枚举。例如我们需要在n个元素选k个,在dfs的时候需要多传入一个参数k,即dfs(cur,n,k)。在每次进入这个dfs函数时,我们都去判断当前temp的长度是否为k,如果为k,就把temp加入答案并直接返回,即:

vector<int> temp;
void dfs(int cur, int n) {// 记录合法的答案if (temp.size() == k) {ans.push_back(temp);return;}// cur == n + 1 的时候结束递归if (cur == n + 1) {return;}// 考虑选择当前位置temp.push_back(cur);dfs(cur + 1, n, k);temp.pop_back();// 考虑不选择当前位置dfs(cur + 1, n, k);
}

这个时候我们可以做一个剪枝,如果当前temp的大小为s,未确定状态的区间[cur,n]的长度为t,如果s+t<k,那么即使t个都被选中,也不可能构造出一个长度为k的序列,故这种情况就没有必要继续向下递归,即我们可以在每次递归开始的时候做一次这样的判断:

if (temp.size() + (n - cur + 1) < k) {return;
}

代码就变成了这样:

vector<int> temp;
void dfs(int cur, int n) {// 剪枝:temp 长度加上区间 [cur, n] 的长度小于 k,不可能构造出长度为 k 的 tempif (temp.size() + (n - cur + 1) < k) {return;}// 记录合法的答案if (temp.size() == k) {ans.push_back(temp);return;}// cur == n + 1 的时候结束递归if (cur == n + 1) {return;}// 考虑选择当前位置temp.push_back(cur);dfs(cur + 1, n, k);temp.pop_back();// 考虑不选择当前位置dfs(cur + 1, n, k);
}

至此,其实我们已经得到了一个时间复杂度为O((nk))的组合枚举,由于每次记录答案的复杂度为O(k),故这里的时间复杂度为O((nk)×k),但是我们还可以进一步优化代码。在上面这份代码中有三个if判断,其实第三处的if是可以被删除的。因为:
【1】首先,cur=n+1的时候,一定不可能出现s>ks是前文中定义的temp的大小),因为自始至终s绝不可能大于k,它等于k的时候就会被第二处if记录答案并返回;
【2】如果cur=n+1的时候s=k,它也会被第二处 if\text{if}if 记录答案并返回;
【3】如果cur=n+1的时候s<k,一定会在cur<n+1的某个位置的时候发现s+t<k,它也会被第一处if剪枝。

因此,第三处if可以删除。最终我们得到了如下的代码。

class Solution {List<Integer> temp = new ArrayList<Integer>();List<List<Integer>> ans = new ArrayList<List<Integer>>();public List<List<Integer>> combine(int n, int k) {dfs(1, n, k);return ans;}public void dfs(int cur, int n, int k) {// 剪枝:temp 长度加上区间 [cur, n] 的长度小于 k,不可能构造出长度为 k 的 tempif (temp.size() + (n - cur + 1) < k) {return;}// 记录合法的答案if (temp.size() == k) {ans.add(new ArrayList<Integer>(temp));return;}// 考虑选择当前位置temp.add(cur);dfs(cur + 1, n, k);temp.remove(temp.size() - 1);// 考虑不选择当前位置dfs(cur + 1, n, k);}
}

时间复杂度: O((k/n​)×k),分析见「思路」部分。
空间复杂度: O(n+k)=O(n),即递归使用栈空间的空间代价和临时数组temp的空间代价。

【2】非递归(字典序法)实现组合型枚举: 这个方法理解起来比「方法一」复杂,建议读者遇到不理解的地方可以在草稿纸上举例模拟这个过程。这里的非递归版不是简单的用栈模拟递归转化为非递归:我们希望通过合适的手段,消除递归栈带来的额外空间代价。假设我们把原序列中被选中的位置记为1,不被选中的位置记为0,对于每个方案都可以构造出一个二进制数。我们让原序列从大到小排列(即{n,n−1,⋯1,0}。我们先看一看n=4k=2的例子:

原序列中被选中的数对应的二进制数方案
43[2][1]00112,1
4[3]2[1]01013,1
4[3][2]101103,2
[4]32[1]10014,1
[4]3[2]110104,2
[4][3]2111004,3
我们可以看出「对应的二进制数」一列包含了由k1n−k0组成的所有二进制数,并且按照字典序排列。这给了我们一些启发,我们可以通过某种方法枚举,使得生成的序列是根据字典序递增的。我们可以考虑我们一个二进制数数字x,它由k1n−k0组成,如何找到它的字典序中的下一个数字next(x),这里分两种情况:
规则一:x的最低位为1,这种情况下,如果末尾由t个连续的1,我们直接将倒数第t位的1和倒数第t+1位的0替换,就可以得到next(x)。如0011→01010101→01101001→10101001111→10101111
规则二:x的最低位为0,这种情况下,末尾有t个连续的0,而这t个连续的0之前有m个连续的1,我们可以将倒数第t+m位置的1和倒数第t+m+1位的0对换,然后把倒数第t+1位到倒数第t+m−1位的1移动到最低位。如0110→10011010→11001011100→11000111

至此,我们可以写出一个朴素的程序,用一个长度为n0/1数组来表示选择方案对应的二进制数,初始状态下最低的k位全部为1,其余位置全部为0,然后不断通过上述方案求next,就可以构造出所有的方案。

我们可以进一步优化实现,我们来看n=5k=3的例子,根据上面的策略我们可以得到这张表:

二进制数方案
001113,2,1
010114,2,1
011014,3,1
011104,3,2
100115,2,1
101015,3,1
101105,3,2
110015,4,1
110105,4,2
111005,4,3

在朴素的方法中我们通过二进制数来构造方案,而二进制数是需要通过迭代的方法来获取next的。考虑不通过二进制数,直接在方案上变换来得到下一个方案。假设一个方案从低到高的k个数分别是{a0,a1,⋯ ,ak−1},我们可以从低位向高位找到第一个j使得aj+1≠aj+1​,我们知道出现在a序列中的数字在二进制数中对应的位置一定是1,即表示被选中,那么aj+1≠aj+1意味着ajaj+1对应的二进制位中间有0,即这两个1不连续。我们把aj对应的1向高位推送,也就对应着aj←aj+1,而对于i∈[0,j−1]内所有的ai把值恢复成i+1,即对应这j1被移动到了二进制数的最低j位。这似乎只考虑了上面的「规则二」。但是实际上 「规则一」是「规则二」在t=0时的特殊情况,因此这么做和按照两条规则模拟是等价的。

在实现的时候,我们可以用一个数组temp来存放a序列,一开始我们先把1k按顺序存入这个数组,他们对应的下标是0k−1。为了计算的方便,我们需要在下标k的位置放置一个哨兵n+1(思考题:为什么是n+1呢?)。然后对这个temp序列按照这个规则进行变换,每次把前k位(即除了最后一位哨兵)的元素形成的子数组加入答案。每次变换的时候,我们把第一个aj+1≠aj+1j找出,使aj自增1,同时对i∈[0,j−1]aia重新置数。如此循环,直到temp中的所有元素为n内最大的k个元素。

回过头看这个思考题,它是为了我们判断退出条件服务的。我们如何判断枚举到了终止条件呢?其实不是直接通过temp来判断的,我们会看每次找到的j的位置,如果j=k了,就说明[0,k−1]内的所有的数字是比第k位小的最后k个数字,这个时候我们找不到任何方案的字典序比当前方案大了,结束枚举。

class Solution {List<Integer> temp = new ArrayList<Integer>();List<List<Integer>> ans = new ArrayList<List<Integer>>();public List<List<Integer>> combine(int n, int k) {List<Integer> temp = new ArrayList<Integer>();List<List<Integer>> ans = new ArrayList<List<Integer>>();// 初始化// 将 temp 中 [0, k - 1] 每个位置 i 设置为 i + 1,即 [0, k - 1] 存 [1, k]// 末尾加一位 n + 1 作为哨兵for (int i = 1; i <= k; ++i) {temp.add(i);}temp.add(n + 1);int j = 0;while (j < k) {ans.add(new ArrayList<Integer>(temp.subList(0, k)));j = 0;// 寻找第一个 temp[j] + 1 != temp[j + 1] 的位置 t// 我们需要把 [0, t - 1] 区间内的每个位置重置成 [1, t]while (j < k && temp.get(j) + 1 == temp.get(j + 1)) {temp.set(j, j + 1);++j;}// j 是第一个 temp[j] + 1 != temp[j + 1] 的位置temp.set(j, temp.get(j) + 1);}return ans;}
}

时间复杂度: O((nk)×k)。外层循环的执行次数是(n/k)次,每次需要做一个O(k)的添加答案和O(k)的内层循环,故时间复杂度O((n/k)×k)
空间复杂度: O(k)。即temp的空间代价。

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

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

相关文章

《对话品牌》——魅力女人:成就幸福人生

本期节目《对话品牌》栏目组邀请到了北京紫琪尔健康管理集团董事长紫琪尔孙红宇女士参加栏目录制&#xff0c;分享其企业故事&#xff0c;树立品牌形象&#xff0c;提升品牌价值&#xff01; 节目嘉宾&#xff1a;紫琪尔集团董事长孙红宇 节目主持人&#xff1a;吴昊 节目播…

前端 -- 基础 常用标签 ( 图像标签及其属性详解)

目录 图像标签 &#xff1a; 插入图像 图像标签的其它属性 &#xff1a; alt 属性 title 属性 width 属性 height 属性 border 属性 注意 图像标签 &#xff1a; 在我们的网页中&#xff0c;可以看到 好多 好多 图片&#xff0c;那这些图片又是怎样 在…

Android画布Canvas drawPath绘制跟随手指移动的圆,Kotlin

Android画布Canvas drawPath绘制跟随手指移动的圆&#xff0c;Kotlin import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Path import android.os.Bundle import android.…

数据库中修改表的语句

1.修改表名&#xff1a;ALTER TABLE 旧表名 RENAME AS 新表名 例&#xff1a;ALTER TABLE teacher RENAME AS teacher1 2.增加表的字段&#xff1a;ALTER TABLE 表名 ADD 字段名 列属性 例&#xff1a;ALTER TABLE teacher1 ADD age INT(11) …

Java 类加载与字节码技术

3 类加载与字节码技术 3.1 类文件结构 类文件结构字节码指令编译期处理类加载阶段类加载器运行期优化 根据 JVM 规范&#xff0c;类文件结构如下 ClassFile {u4 magic;u2 minor_version; // 小版本号u2 major_version; // 主版本号u2 constant_pool_count; // 常量池cp_info…

算法时间空间复杂度计算—空间复杂度

算法时间空间复杂度计算—空间复杂度 空间复杂度定义影响空间复杂度的因素算法在运行过程中临时占用的存储空间讲解 计算方法例子1、空间算法的常数阶2、空间算法的线性阶&#xff08;递归算法&#xff09;3、二分查找分析方法一&#xff08;迭代法&#xff09;方法二&#xff…

WEB 3D技术 three.js 色彩空间讲解

上文 WEB 3D技术 three.js 设置环境贴图 高光贴图 场景设置 光照贴图 我们讲了基础材质的各种纹理 但是 我们的图片 到了界面场景中 好像绿的程度有点不太一样了 这里的话 涉及到我们的色彩空间 他有两种 一种是线性的 一种是 sRGB类型的 线性呢 就是根据光照强度 去均匀分…

【满分】【华为OD机试真题2023CD卷 JAVAJS】API集群负载统计

华为OD2023(C&D卷)机试题库全覆盖,刷题指南点这里 API集群负载统计 时间限制:1s 空间限制:256MB 限定语言:不限 题目描述: 某个产品的RESTful API集合部署在服务器集群的多个节点上,近期对客户端访问日志进行了采集,需要统计各个API的访问频次,根据热点信息在服务…

拓扑排序图解-Kahn算法和深度优先搜索

拓扑排序 是将一个有向无环图中的每个节点按照依赖关系进行排序。比如图 G G G存在边 < u , v > <u,v> <u,v> 代表 v v v的依赖 u u u, 那么在拓扑排序中&#xff0c;节点 u u u一定在 v v v的前面。 从另一个角度看&#xff0c;拓扑排序是一种图遍历&#…

KMP算法的理解+板子

对kmp算法的理解中&#xff0c;很重要的一点就是next数组。 很多人不理解next数组的含义&#xff0c;是因为它同时具有两个意思&#xff0c;而且这两个意思在不同的环境下不同。 现在给你两个字符串&#xff1a; 一个是文本串 text 一个是模板串 pattern 然后定义两个指针…

《别让猴子跳回背上》——管理者的时间管理

讲时间管理的书很多&#xff0c;但这本是专门讲给管理者的时间管理。 在职场中&#xff0c;许多管理者都会碰到工作计划执行不下去、组织目标难于实现的问题&#xff0c;搭进了自己所有可以支配的时间&#xff0c;仍旧是焦头烂额&#xff0c;顾此失彼&#xff1b;而下属则因为…

力扣题目学习笔记(OC + Swift)19. 删除链表的倒数第 N 个结点

19. 删除链表的倒数第 N 个结点 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 此题目为链表题&#xff0c;拿出我们的杀手锏&#xff0c;链表解题经典三把斧&#xff1a; 哑巴节点栈快慢指针 关于内存问题&#xff1a;由于Swift及…

算法基础day1

归并排序模版 #include <iostream> using namespace std; int n; const int N 1e610; int q[N],tmp[N]; void merge_sort(int l,int r,int q[]){if(l>r) return;int mid lr>>1;merge_sort(l,mid,q);merge_sort(mid1,r,q);//归并的的过程int k0,il,jmid1;while(…

【Vue2+3入门到实战】(10)Vue基础之一文掌握 组件通信 详细示例(组件通信语法、父传子 、 子传父、非父子通信)

这里写自定义目录标题 一、学习目标1.组件通信2.综合案例&#xff1a;小黑记事本&#xff08;组件版&#xff09; 二、组件通信1.什么是组件通信&#xff1f;2.组件之间如何通信3.组件关系分类4.通信解决方案5.父子通信流程6.父向子通信代码示例7.子向父通信代码示例8.总结 三、…

代码随想录 122. 买卖股票的最佳时机 II

题目 给你一个整数数组 prices &#xff0c;其中 prices[i] 表示某支股票第 i 天的价格。 在每一天&#xff0c;你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买&#xff0c;然后在 同一天 出售。 返回 你能获得的 最大 利润 。 示例 …

FME之读取文件名路径FilenamePartExtractor转换器

在读取文件所在路径及相关信息时&#xff0c;我们除了在读模块时选择Directory and File Pathnames数据类型。还可以选择在某个阶段使用FilenamePartExtractor转换器来读取文件所在路径及相关信息。 在前面转换器只要暴露有fme_dataset&#xff0c;在源文件名选择它即可实现。…

Google Play上架:2023年度总结报告

今天是2023年的最后一个工作日&#xff0c;今天用来总结一下2023年关于谷歌商店上架的相关政策改动和对应的拒审解决方法。 目录 政策更新与改动2023 年 2 月 22 日2023 年 4 月5 日2023 年 7 月 12 日2023 年 10 月 25 日 开发者计划政策拒审邮件内容和解决办法 政策更新与改…

利用Pandas进行高效网络数据获取

利用Pandas进行高效网络数据获取 背景&#xff1a; ​ 最近看到一篇关于使用Pandas模块进行爬虫的文章&#xff0c;觉得很有趣&#xff0c;这里为大家详细说明。 基础铺垫&#xff1a; ​ pd.read_html pandas 库中的一个函数&#xff0c;用于从 HTML 页面中读取表格数据并…

2.如何设置vscode可以搜索node_modules里的内容

在setting.json里添加 {"search.exclude": {"**/node_modules":false},"search.useIgnoreFiles":false }1.将**/node_modules设为false&#xff0c;从而禁用缺省行为&#xff0c;从而支持node_modules目录的检索 对于许多项目而言&#xff0c;将…

数据库 基础面试第一弹

1. SQL语句类型 1. DDL&#xff08;Data Definition Language&#xff0c;数据定义语言&#xff09;&#xff1a; DDL语句用于定义数据库对象&#xff08;如表、索引、视图等&#xff09;。常见的DDL语句包括&#xff1a; CREATE&#xff1a;用于创建数据库对象&#xff0c;如…