代码随想录第二十二天 | Leecode 77. 组合、216. 组合总和 III、17. 电话号码的字母组合
Leecode 77. 组合
题目描述
给定两个整数 n 和 k,返回范围 [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]]
解题思路与代码展示
本题要求给出所有满足组合情况,使用穷举法逐个尝试,如果满足则存入结果数组中。但直接写的过程中会遇到两个比较难处理的点:
- 如果每次要求的数的个数固定,比如满足条件的3个数存入数组中,那么就只需使用3层for循环即可,即需要几个数就使用几层for循环。但题目所给的数的个数并不固定,故此时需要使用回溯(递归)的方式来进行处理。
 - 此外,还需要确保输出的数组没有重复,即每次搜索过的数不会进行二次搜索。为了避免重复查找,可以设置一个查找的初始值,每次递归的时候传入该值并从这个值开始往后递归搜索。
 
综合上面思路,可以写出代码如下:
class Solution {
public:
    vector<vector<int>> result; // 使用两个全局变量来存储结果值和过程中每一次搜索的结果
    vector<int> curVec;
    void combineHelper(int n, int k, int startindex){ // 递归函数
        if(curVec.size() == k) { // 如果vector当前大小为k,则说明已经符合条件,直接push存放入结果中
            result.push_back(curVec);
            return;
        }
        for(int i = startindex; i <= n-(k-curVec.size()) + 1; i++){ // 使用for循环遍历,相当于每次递归调用都有一层for循环;并从startindex开始搜索。其中终止条件中i的上界是剪枝后的结果,排除了一些不可能的情况,可以加速搜索。
            curVec.push_back(i);  // 将当前数存入当前vector中
            combineHelper(n, k, i+1); // 递归调用
            curVec.pop_back(); // 回溯退回
        }
    }
    vector<vector<int>> combine(int n, int k) {
        combineHelper(n,k,1); // 递归调用辅助函数
        return result; // 返回结果
    }
};
Leecode 216. 组合总和 III
题目描述
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
- 只使用数字
1到9 - 每个数字 最多使用一次
 
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
- 示例 1:
 
输入:
k = 3,n = 7
输出:[[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。
- 示例 2:
 
输入:
k = 3,n = 9
输出:[[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。
- 示例 3:
 
输入:
k = 4,n = 1
输出:[]
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是 1 + 2 + 3 + 4 = 10,因为10 > 1,没有有效的组合。
解题思路与代码展示
本题和上面一题比较类似,使用相似的方法即可,区别在于递归终止条件中要多考虑如果求和值大于目标的情况,此时不满足条件,直接退回即可。
class Solution {
public:
    vector<int> curVec; // 使用vector和curSum来记录当前状态,用于判断是否满足条件
    int curSum;
    vector<vector<int>> result; // result用于存放最终结果
    void combHelper(int k, int n, int startNum){
        if(curSum == n && curVec.size() == k){ // 如果数求和与数的个数都恰好满足条件,则说明此时已经满足
            result.push_back(curVec); // 将满足条件的vector存入结果中
        }
        else if(curSum > n) return; // 如果求和大于目标值,则一定不满足,此时直接返回即可
        for(int i = startNum; i < 10 ; i++){ // 每一位数都从startNum开始递归求解
            curSum += i; // 更新当前总和,更新存储的vec向量
            curVec.push_back(i);
            combHelper(k,n,i+1); // 递归搜索下一个数
            curSum -= i; // 回溯,此时要返回上一次的状态,故需要同时恢复curSum和curVec
            curVec.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        combHelper(k, n, 1);
        return result;
    }
};
上面代码关键在于递归返回进行回溯时,需要恢复所有递归过程中被修改的变量,在本题中需要同时回复curSum和curVec。
Leecode 17. 电话号码的字母组合
题目描述
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

- 示例 1:
 
输入:
digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
- 示例 2:
 
输入:
digits = ""
输出:[]
- 示例 3:
 
输入:
digits = "2"
输出:["a","b","c"]
解题思路与代码展示
本题思路上不算难,在确定了使用回溯的方法后,只需要根据每次数字的不同从相应可能得字母字符中依次选取进行回溯递归即可。但困难的点在于,总共有字母的按键包括2-9总共8个键,如果每一个键用一个if,同时内部使用一个for循环进行回溯,会导致写很多没必要的重复代码。为了避免这种情况需要使用一些方法来进行处理。我首先想到的第一种方法是合并前边2-6个数字,因为前5个数每个数都刚好对应3个字母,因此可以使用乘除法和取余运算来进行计算当前字母对应哪些字母。但需要注意的是,这个计算过程中需要非常小心int类型与char类型之间的转换,1的ASCII码并不等于1,中间需要加减一个'0'才能进行转换。
使用这种方法可以写出代码如下:
class Solution {
public:
    vector<string> result; // 同样是使用全局变量来存储结果和当前状态的字符串
    string curStr;
    void combinationHelper(string digits, int curIndex){
        if(curIndex == digits.size()) { // 如果所有数字已经遍历结束,则将当前字符串存入结果中,并返回
            result.push_back(curStr);
            return;
        }
        if(digits[curIndex] < '7'){ // 对于前2-6的数,每个数都对应三个字母,故可以放在一起处理
            for(int i = 0; i < 3; i++){ // for循环遍历每个数对应的三个字母
                curStr += ('a' + (digits[curIndex]-'0'-2)*3 ) + i%3; // 存入一个字母,此时要注意字符与int之间转换需要减去一个值
                combinationHelper(digits, curIndex+1); // 递归调用存入下一个数字对应的字母
                curStr.pop_back(); // 回溯
            }
        }
        else if(digits[curIndex] == '7' ){ // 处理完前2-6之后,采用同样的方式处理7,8,9,区别在于7和9对应4个字母,其余都一样
            for(int i = 0 ; i < 4; i++){
                curStr += 'p' + i;
                combinationHelper(digits, curIndex+1);
                curStr.pop_back();
            }
        }
        else if(digits[curIndex] == '8'){
            for(int i = 0; i < 3; i++){
                curStr += 't' + i;
                combinationHelper(digits, curIndex+1);
                curStr.pop_back();
            }
        }
        else if(digits[curIndex] == '9'){
            for(int i = 0 ; i < 4; i++){
                curStr += 'w' + i;
                combinationHelper(digits, curIndex+1);
                curStr.pop_back();
            }
        }
    }
    vector<string> letterCombinations(string digits) {
        if(!digits.size()) return result;  // 如果输入的字符串为空,则直接返回
        combinationHelper(digits, 0); // 使用回溯递归调用,将所有可能结果都存入result
        return result; // 返回result结果
    }
};
可以看到,上面代码虽然确实在合并了一些情况的同时也能够处理本题中的所有情况,但是对于数字取到7-9时,还是有大量重复代码。为了简化这部分内容,考虑使用一个string类型的数组来存放每个数字对应的字母。即:
string numLetterMap[10]{
     "",
     "",
     "abc",
    "def",
    "ghi",
    "jkl",
    "mno",
    "pqrs",
    "tuv",
    "wxyz"
};
这样每次使用将按键数字转换为字母字符的时候,直接去数组中查找即可,这样我们可以修改得到下面的代码:
class Solution {
public:
    string numLetterMap[10]{ // 存放每个按键对应的字母字符
    "",
    "",
    "abc",
    "def",
    "ghi",
    "jkl",
    "mno",
    "pqrs",
    "tuv",
    "wxyz"
    };
    void combHelper(vector<string>& result, string digits, string curStr){
        if(curStr.size() == digits.size()){ // 如果已经长度相等,说明已经完成,此时需要存入结果中
            result.push_back(curStr);
            return;
        }
        int num = digits[curStr.size()] - '0'; // 首先将char类型的数字转换为int类型
        for(int i = 0; i < numLetterMap[num].size(); i++){ // for循环下遍历所有取值,注意此时循环次数取决于字符串数组中对应位置的长度
            curStr += numLetterMap[num][i]; // 当前字符串上加一个字母
            combHelper(result, digits, curStr); // 递归
            curStr.pop_back(); // 回溯
        }
    }
    vector<string> letterCombinations(string digits) {
        vector<string> result;
        if(!digits.size()) return result;
        string curStr = "";
        combHelper(result, digits ,curStr);
        return result;
    }
};
通过上面代码可以学到,之后再遇到讨论情况比较多的时候,为了避免使用多个if或是switch等造成讨论情况过多,可以考虑使用一些数据类型来存储一部分信息,从而简便讨论。
今日总结
今天学习到了回溯算法,了解到回溯和递归的密不可分。回溯过程中会有一个或多个变量用于存放“当前状态”,每一次使当前状态变为下一状态之后调用递归函数进行处理,递归结束之后还需要将当前进行恢复。同时回溯可以考虑使用一个全局变量来存放结果和当前状态,或者是用引用传递的方式来确保可以进行修改。
此外,当遇到需要讨论的情况比较多的时候,考虑能否使用一个数组来存储不同情况下的一些取值,从而减少if讨论分支,减少代码量。
力扣当前刷到82题了,继续坚持
代码随想录第二十二天 | Leecode 77. 组合、216. 组合总和 III、17. 电话号码的字母组合的更多相关文章
- LeetCode ● 216.组合总和III ● 17.电话号码的字母组合
		
LeetCode 216.组合总和III 分析1.0 回溯问题 组合总和sum == n 时以及path中元素个数 == k 时,res.add(new path) 返回后递归删除掉当前值 class ...
 - 代码随想录第二天| 977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II
		
2022/09/22 第二天 第一题 这题我就直接平方后排序了,很无脑但很快乐啊(官方题解是双指针 第二题 滑动窗口的问题,本来我也是直接暴力求解发现在leetCode上超时,看了官方题解,也是第一次 ...
 - javaSE第二十二天
		
第二十二天 312 1:登录注册IO版本案例(掌握) 312 2:数据操作流(操作基本类型数据的流)(理解) 313 (1)定义: 313 (2)流对象名称 313 (3 ...
 - 《代码大全(第二版)》【PDF】下载
		
<代码大全(第二版)>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230382264 内容简介 <代码大全(第2版)>是著 ...
 - 孤荷凌寒自学python第二十二天python类的继承
		
孤荷凌寒自学python第二十二天python类的继承 (完整学习过程屏幕记录视频地址在文末,手写笔记在文末) python中定义的类可以继承自其它类,所谓继承的概念,我的理解 是,就是一个类B继承自 ...
 - [LeetCode] 216. Combination Sum III 组合之和 III
		
Find all possible combinations of k numbers that add up to a number n, given that only numbers from ...
 - 代码随想录第十三天 | 150. 逆波兰表达式求值、239. 滑动窗口最大值、347.前 K 个高频元素
		
第一题150. 逆波兰表达式求值 根据 逆波兰表示法,求表达式的值. 有效的算符包括 +.-.*./ .每个运算对象可以是整数,也可以是另一个逆波兰表达式. 注意 两个整数之间的除法只保留整数部分. ...
 - 代码随想录第八天 |344.反转字符串 、541. 反转字符串II、剑指Offer 05.替换空格 、151.翻转字符串里的单词 、剑指Offer58-II.左旋转字符串
		
第一题344.反转字符串 编写一个函数,其作用是将输入的字符串反转过来.输入字符串以字符数组 s 的形式给出. 不要给另外的数组分配额外的空间,你必须原地修改输入数组.使用 O(1) 的额外空间解决这 ...
 - 【LeetCode动态规划#05】背包问题的理论分析(基于代码随想录的个人理解,多图)
		
背包问题 问题描述 背包问题是一系列问题的统称,具体包括:01背包.完全背包.多重背包.分组背包等(仅需掌握前两种,后面的为竞赛级题目) 下面来研究01背包 实际上即使是最经典的01背包,也不会直接出 ...
 - ruby代码重构第二课
		
(文章都是从我的个人主页上粘贴过来的, 大家也可以访问我的主页 www.iwangzheng.com) 在第一课里提取出了相通的代码,第二课里就把常量提取出来吧 一般把常量的定义写的对应的app/mo ...
 
随机推荐
- 使用Docker编译安装运行Doris
			
一.编译源码 (1)拉取编译镜像docker pull apache/incubator-doris:build-env-1.2 (2)Mac电脑上拉取源码git clone https://gith ...
 - C# Winform 当音频播放完成后,播放下一个音频,怎么知道音频有没有播放完成
			
程序在预警时,会发出报警音,当报警音频播放时间,超过预警频率时,就会像我们打印文档一样,像打印机发送10次打印任务.当打出第1张纸的时候,这时候想取消打印.就不能在电脑端通过软件操作了.因此为了避免这 ...
 - MAC消息认证码介绍
			
此MAC是密码学概念,与计算机网络不同 为什么有了摘要算法还要有MAC 摘要算法保障的是消息的完整性 归根到底就是由H(x)来保证x的完整 那么问题来了,如果我知道你所使用的摘要算法(例如中间人攻击) ...
 - Esp32s3(立创实战派)移植LVGL
			
Esp32s3(立创实战派)移植LVGL 移植: 使用软件EEZ studio 创建工程选择带EEZ Flow的,可以使用该软件更便捷的功能 根据屏幕像素调整画布为320*240 复制ui文件至工程 ...
 - 关于企业微信扫码登陆vue
			
关于企业微信扫码登陆vue 企业微信扫码登陆官方文档 采用的是第一种(构造独立窗口登录二维码) 对于前端来说就步骤就是 页面展示二维码 => 用户扫码登陆点击确定 => 确定之 ...
 - Typecho 如何开启 HTTPS
			
一般来说,我们直接开启 HTTPS 就行,开启后进去网站后台修改网站的 URL 即可. 但是我昨天发现,我的工具箱迁移服务器之后,前台看着是很正常的,但是后台的登陆页面引入的依然的 http 标头,所 ...
 - Linux - sshpass的安装与使用
			
ssh 登陆不能在命令行中指定密码,sshpass 的出现则解决了这一问题.它允许你用 -p 参数指定明文密码,然后直接登录远程服务器,它支持密码从命令行.文件.环境变量中读取. 安装 1.下载ssh ...
 - [WinUI 3] 模仿 Visual Studio 的 Docking 控件
			
WinUI 3 是什么? WinUI 3 是微软前几年推出的一款 UI 框架,它是 UWP 的升级版,支持 Win32 和 WinUI 3 混合开发.并且 WinUI 3 的设计风格更加现代化. 无论 ...
 - Vue3生命周期钩子函数深度解析:从源码到实战的万字指南
			
一.Vue3生命周期革新特性 相较于Vue2,Vue3通过Composition API带来了更灵活的生命周期管理方式.通过onBeforeMount等函数注册钩子时,实际是通过injectHook方 ...
 - VirtualBox磁盘扩容
			
前言 虚拟机开始时设置的磁盘空间比较小,后面使用就不够了. # 查询磁盘使用情况 df -h 虚拟硬盘扩容 关闭正在运行的虚拟机 选中工具栏 选择虚拟硬盘,并选中需要扩容的磁盘 拖动进度条,设置想要扩 ...
 
			
		