DAY28:回溯算法(三)组合总和Ⅲ+电话号码字母组合

news2024/11/16 5:45:22

文章目录

    • 216.组合总和Ⅲ
      • 思路
        • 树形结构
      • 完整版
      • debug测试
        • 逻辑错误:没有输出
      • 剪枝操作
      • 剪枝版本
        • continue的用法
        • 剪枝最后是continue还是return的讨论
    • 17.电话号码的字母组合
      • 思路
        • 树形结构
      • 伪代码
        • 字符串中的字符'2'转化成int的方法
        • 字符串字符与int转换补充
        • 字符串与字符
      • 完整版
        • 补充:为啥这里不考虑剪枝

216.组合总和Ⅲ

找出所有相加之和为 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,没有有效的组合。

提示:

  • 2 <= k <= 9
  • 1 <= n <= 60

思路

本题和组合问题的区别就是,组合问题是一共n个数,找到k个数字的所有组合。本题也是找k个数,但是要求这k个数的和是n。

k个数字只能从1–9里面选择。组合问题的n也是限制了1–n的范围。本题相当于在1–9的数字里,选出k个数字的组合,使得其和为n。也就是在组合问题的基础上,加上了和为n的限制

我们先考虑最简单的k=2,n=4的情况,这种情况下在1–9中选择两个元素令其和=4,就是嵌套两层for循环。当k=3,就是嵌套3层for循环。

当k是n的时候,也就是嵌套n层for循环。此时暴力想法做不出来,就考虑使用回溯。回溯就是用来递归,控制for的嵌套层数

树形结构

例如k=2 n=4的情况,[1,9]取数字:

在这里插入图片描述
回溯的深度是由K来决定的,k越大,查找深度就越深,因为k越大就要确定越多的数字。K控制了树的深度

树的宽度是1–9控制的,1–9分出去的分支就是树的宽度

完整版

  • startIndex取i+1因为组合里不能有重复元素
  • 本题和组合比较像
//此处结果并不需要返回什么,因为结果都存在result里面了
//需要单独定义targetSum和sum,来存放目标和与当前和的数据
//同时还需要定义控制循环开始位置的startIndex!
void backtracking(vector<int>& path,vector<vector<int>>&result,int k,int targetSum,int sum,int startIndex){
    //终止条件
    if(path.size()==k){
        //检查和是否符合要求
        if(sum==targetSum){
            result.push_back(path);
        }
        return;
    }
    
    //单层搜索
    for(int i=startIndex;i<=9;i++){
        //本层累加
        sum = sum+i;
        path.push_back(i);
        //递归for循环,取了[1]之后再取[1,2][1,3][1,4]……
        backtracking(path,result,k,targetSum,sum,i+1);
        //回溯,开始取[2],后面[2,3][2,4]……
        path.pop_back();
        sum = sum-i;
    }

}

//主函数,传参和赋初值可以先写
vector<vector<int>> combinationSum3(int k, int n) {
	int sum=0;
    int startIndex=1;
    vector<int>path;
    vector<vector<int>>result;
    backtracking(path,result,k,n,sum,startIndex);
    return result;
}

debug测试

逻辑错误:没有输出

我们把最开始的版本贴进去之后,发现并没有输出
在这里插入图片描述
这个问题主要是出在单层搜索的回溯上面。

因为本题是累加sum并且在每一层中判断sum的值是不是等于目标值,因此,sum在每一层都是当前层递归的特定值,必须也进行回溯操作!

没有输出就是因为sum没有回溯,导致1的所有组合计算sum结束了之后,遍历到2的时候,sum值还是在1的所有组合sum基础上进行累加!这使得 sum 的值变得过大,所以 if(sum == targetSum) 条件很难满足,从而使结果集无法被填充。此时如果1中没有符合要求的条件,就不可能有任何输出,因为结果集是空的。

也就是说,不回溯的话只能找得到1开头的符合条件的组合,从2开头起就很难填充结果集了。因为当处理完1开头的所有可能组合之后,sum 的值实际上是1开头的组合的和。

单层逻辑修改为:

  • 一定要注意sum也要回溯,这是累加的结果值
	//单层搜索
    for(int i=startIndex;i<=9;i++){
        //本层累加
        sum = sum+i;
        path.push_back(i);
        //递归for循环,取了[1]之后再取[1,2][1,3][1,4]……
        backtracking(path,result,k,targetSum,sum,i+1);
        //回溯,开始取[2],后面[2,3][2,4]……
        path.pop_back();
        //一定要注意sum也要回溯,这是累加的结果值!
        sum = sum-i;
    }

剪枝操作

本题的剪枝操作和组合有一部分类似,本题是固定在[1,9]内部进行搜索,也可能存在遍历到i,剩下的元素本身已经<k的情况。

也就是剩下的元素 < 还需要加入的元素,9-i+1 < k - path.size(),即为i>9-( k - path.size())+1

另一个剪枝是关于和sum的剪枝,也就是没到k的情况下,当前数字的sum值如果已经大于targetSum,那么,就不可能存在到了k之后=targetSum的情况了。

剪枝版本

  • 剪枝一定要注意剪枝的同时要把回溯做了
//注意剪枝的同时,直接返回,必须要剪枝同时把回溯也做了
void backtracking(vector<int>& path,vector<vector<int>>&result,int k,int targetSum,int sum,int startIndex){
    //终止条件
    if(path.size()==k){
        //检查和是否符合要求
        if(sum==targetSum){
            result.push_back(path);
        }
        //==k无论如何都会return
        return;
    }
    
    //单层搜索
    for(int i=startIndex;i<=9-(k-path.size())+1;i++){
        //本层累加
        sum = sum+i;
        path.push_back(i);
        
        //如果此时的sum已经比targetSum要大,那么已经可以剪枝去找下一个了
        if(sum>targetSum){
            //剪枝,剪枝的时候一定要记得回溯!
            sum = sum-i;
            path.pop_back();
            //这里最好还是写continue,跳过for循环剩下所有部分进行下一次for循环
            continue;
        }
        
        //递归for循环,取了[1]之后再取[1,2][1,3][1,4]……
        backtracking(path,result,k,targetSum,sum,i+1);
        //回溯,开始取[2],后面[2,3][2,4]……
        path.pop_back();
        sum = sum-i;
    }

}

//主函数,传参和赋初值可以先写
vector<vector<int>> combinationSum3(int k, int n) {
	int sum=0;
    int startIndex=1;
    vector<int>path;
    vector<vector<int>>result;
    backtracking(path,result,k,n,sum,startIndex);
    return result;
}

continue的用法

在C++中,continue语句用于跳过当前循环中剩余的代码,然后直接开始下一次循环。在我们代码这种情况下,continue跳过当前for循环中 backtracking 函数之后的所有代码(包括回溯操作和sum的恢复),并且立即开始下一次for循环

这对于剪枝操作是有意义的,因为如果sum已经大于targetSum,那么就没有必要再进一步搜索这个路径了,直接跳过剩下的步骤并尝试下一个可能的数会更有效率。然而,这不意味着可以忽略回溯操作,还是需要在continue之前将pathsum恢复到他们的原始状态,这样才能确保在下一次迭代中pathsum的值是正确的。

但是这道题剪枝操作,一开始把continue写成了return,其实也能过,但是是因为本题的特殊性。最好还是用continue。

(实际上这道题的特性,如果sum已经大于target值了,那么continue再换更大的数字肯定不满足条件,所以直接return也是对的。)

剪枝最后是continue还是return的讨论

使用 return直接终止当前的函数调用,回到调用者那里。在这种情况下,return 会终止当前的 backtracking 函数调用,返回到上一层的 backtracking 函数或 combinationSum3 函数。这意味着会直接跳过当前的 for 循环中的其余迭代,可能会错过一些有效的解

使用 continue跳过当前的循环体中的剩余部分,直接开始下一次迭代。在这种情况下,continue 会跳过当前迭代的剩余部分,直接开始下一次迭代。这样,你可以正确地检查所有可能的 i 值,而不会错过任何可能的解。

17.电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

在这里插入图片描述
示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

示例 2:

输入:digits = ""
输出:[]

示例 3:

输入:digits = "2"
输出:["a","b","c"]

提示:

  • 0 <= digits.length <= 4
  • digits[i] 是范围 ['2', '9'] 的一个数字。

思路

本题需要注意数字对字符串的映射。可以用map做映射也可以用二维数组做映射

二维数组做映射,就是说数组是字符串类型数字是对应的下标下标2对应字符串"abc",以此类推

例如输入"23",比较直观的想法是写两个for循环,一个是2的字符串组合,一个是3的字符串组合,再输出2对应字母和3对应字母的组合。

如果是3个数字,就是3个for循环列出3个对应字母的字符组合。n个for循环,就是回溯来解决。

树形结构

树形结构如下图所示。这棵树的深度是由输入数字的个数来确定的。输入多个数字,就需要在多个数字里面进行选择,每个数字选一个对应字符串里面的字母。

树的宽度是由每个数字对应的字母长度来确定的,比如2对应字母"abc",树宽度就是abc(三种取值)

在这里插入图片描述

伪代码

  • backtracking定义的参数一般都是结果集+单个想要的结果!比如path和result
  • for循环的嵌套,需要传入startIndex来控制循环起点的一般是在一个集合里面求组合,需要控制这一个集合里面的循环起点避免得到重复组合;而本题是两个集合找元素进行组合,并不需要startIndex来控制之前遍历过哪些元素
  • 但是仍然需要一个index,告诉我们字符串digits中,现在遍历到哪个数字了!方便写终止条件
  • 本题是两个集合里面去取元素的组合,不需要控制,直接i=0即可
//先定义收取结果的东西,也就是总结果集vector<string>和单个想要的结果的string
//for循环的嵌套,需要传入startIndex来控制循环起点
void backtracking(string s,vector<string>& result,string digits,int index){
    //终止条件,如果正在遍历的元素到了size,也就是指向末尾了
    //这里不是size()-1!因为最后一个也要处理!
    if(index==digit.size()){
        result.push_back(s);
        return;
    }
    //单层遍历逻辑
    //先取出数字
    int digit = digits[index]-'0';//index表示本层递归取出了哪个数字,注意传入的是字符串"2"的时候,需要减掉'0',去做下标
    //获取数字对应字符串
    string letters = letterMap[digit];//digit作为下标  本题需要单独写一个letterMap存放对应字符串
    //遍历字符串
    for(int i=0;i<letters.size();i++){
        //加入第一个字母'a'
        s.push_back(letters[i]);
        //隐含index的回溯,递归得到"ad""ae""af"
        backtracking(s,result,digits,index+1);
        //'a'弹出,继续'b'开头
        s.pop_back();
    }
    
}

字符串中的字符’2’转化成int的方法

由于字符串"23"中含有的是字符’2’和’3’,因此我们不能直接对字符进行下标转换,必须将字符转换为int。

注意,字符串中的字符’2’就是可以进行运算的ascii码的形式

int digit = digits[index]-'0';//此处就是将字符'2'和'3'转化为int 2和3

类似的用法: 有效字母异位词

int record[26]={0}; //注意数组的初始化方式
for(int i=0;i<s.size();i++){ 
     record[s[i]-'a']++; //此处就是将'b'等字符转化成数组下标,0对应a,1对应b
}

类似这样的,数字字符字母字符整数之间的转换,以及将其作为数组下标的操作,在处理这类字符串和字符相关的问题中是非常常见的。

将转换后的字符对应的int作为数组或者哈希表的下标,可以用来记录字符的频率数组中查询字符对应的子集。比如本题,我们使用这个整数作为letterMap数组的下标查找到对应的字符集。检查频率和下标查找在处理字符串相关的问题中很有用。

字符串字符与int转换补充

在许多编程语言中,字符是用ASCII值来表示的,每个字符都有一个对应的整数。所以可以通过计算字符之间的差值来完成想要的转换。

例如本题,想把字符’2’转化成整数(数组下标)2,那么需要做的是’2’ - ‘0’,因为在ASCII表中,字符’2’的值为50,字符’0’的值为48,他们之间的差值就是2。

同理,如果想把字符’b’转化成数组下标1(假设’a’对应0),需要做的是’b’ - ‘a’,因为在ASCII表中,字符’b’的值为98,字符’a’的值为97,他们之间的差值就是1。

通常来说,如果我们想将数字的字符转为数组下标,我们可以将该字符与’0’的ASCII值做差,这样就可以得到该数字字符对应的整数值。例如,字符’2’转为整数2,可以用’2’ - '0’实现。

类似地,如果我们想将小写字母字符转为一个序列号(假设’a’对应0,‘b’对应1等等),我们可以将该字符与**‘a’**的ASCII值做差,这样就可以得到该字母字符对应的序列号。例如,字符’b’转为整数1,可以用’b’ - 'a’实现。

字符串与字符

在大多数编程语言中,使用单引号(')括起来的是字符,而使用双引号(")括起来的是字符串字符对应一个ASCII值,可以进行数学运算,而字符串则是一系列字符的集合,一般不能直接进行数学运算

也就是说**"a"表示的就是字符串**!双引号的字符串是不能进行加减运算的,只有单引号的ASCII码可以,例如’a’

字符串中,'a’表示的是ASCII码的a也就是97,'A’表示的是ASCII码也就是65。

在’A’和’a’之间,除了大小写字母之外,其实还是存在一些符号的!因为65+26 = 91,而’a’的值是97。

在ASCII编码表中,'A’至’Z’的ASCII值是从65至90'a’至’z’的ASCII值是从97至122。在’Z’(ASCII值90)和’a’(ASCII值97)之间还有6个字符,它们分别是:‘[’(91),‘’(92),‘]’(93),‘^’(94),‘_’(95)和’`'(96)。

完整版

  • 注意接收的路径path需要定义成string s而不是vector
  • 输入为空的特殊用例,直接加if就行了
  • 注意数组定义赋值用的是大括号{}
class Solution {
public:
//注意数组的初始化方式
    string letterMap[10]={
        "", //是逗号不是分号
        "",
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz",
    };
    void backtracking(string path,vector<string>&result,int index,string digits){
        //终止条件
        if(path.size()==digits.size()){
            result.push_back(path);
            return;
        }
        //单层搜索,先得到第一个遍历的数字
        int digitsNum = digits[index]-'0';
        //第一个遍历的数字的字符串
        string a = letterMap[digitsNum];
        for(int i=0;i<a.size();i++){
            path.push_back(a[i]);
            backtracking(path,result,index+1,digits);
            //递归收集'a'开头结束之后,去找'b'开头
            path.pop_back();//pop里面没有参数,error: too many arguments to function call, expected 0, have 1
        }
    }
    vector<string> letterCombinations(string digits) {
        int index=0;
        vector<string>result;
        string path;
        if(digits.size()==0){
            return result;
        }
        backtracking(path,result,index,digits);
        return result;
    }
};

补充:为啥这里不考虑剪枝

电话号码这道题其实并不算组合问题。组合型问题,就是可以剪枝优化的。但是子集型问题很难剪枝,电话号码这道题实际上是子集型问题,并不是组合型问题。

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

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

相关文章

CTF-Show密码学【摩斯码、培根密码】

萌新 密码33 一、题目信息 题目名称&#xff1a;我想吃培根题目描述&#xff1a;-- — .-. … . …–.- … … …–.- -.-. — — .-… …–.- -… …- - …–.- -… .- -.-. — -. …–.- … … …–.- -.-. — — .-… . .-. …–.- – – -… -… – -… – -… – – – -…

ASP.NET Core MVC 从入门到精通之缓存

随着技术的发展&#xff0c;ASP.NET Core MVC也推出了好长时间&#xff0c;经过不断的版本更新迭代&#xff0c;已经越来越完善&#xff0c;本系列文章主要讲解ASP.NET Core MVC开发B/S系统过程中所涉及到的相关内容&#xff0c;适用于初学者&#xff0c;在校毕业生&#xff0c…

理解KMP

KMP 问题 字符串匹配问题&#xff0c;问字符串 str1中是否存在连续的子串与字符串str2相等,存在返回子串的起始位置&#xff0c;否则返回-1 思路 传统做法是依次遍历str1中的每个字符作为起始位置&#xff0c;看是否能凑出字符串str2. KMP算法就是对传统做法的一种加速&am…

【Linux】软件包管理器yum与环境开发工具vim

【Linux】系列文章目录 【Linux】基础常见指令&#xff1a;http://t.csdn.cn/hwLPb 【Linux】基本权限&#xff1a;http://t.csdn.cn/faFZg 目录 前言 一、软件包管理器yum 1.什么是软件包&#xff1f; 2. yum的使用 &#xff08;1&#xff09;包文件查询 &#xff08…

EMA:基于跨空间学习的高效多尺度注意力模块

文章目录 摘要1、 简介2、相关工作3、高效多尺度注意力机制3.1、回顾坐标注意力(CA)3.2、多尺度注意力(EMA)模块 4、实验4.1、CIFAR-100上的图像分类4.2、ImageNet-1k上的图像分类4.3、MS COCO上的目标检测4.4、基于VisDrone的目标检测 5、消融实验6、 结论 摘要 https://arxi…

MIT 6.S081 Lab Six

MIT 6.S081 Lab Six 引言Copy-on-Write Fork for xv6问题解决方案Implement copy-on write (hard)代码解析 可选的挑战练习 引言 本文为 MIT 6.S081 2020 操作系统 实验六解析。 MIT 6.S081课程前置基础参考: 基于RISC-V搭建操作系统系列 Copy-on-Write Fork for xv6 虚拟内…

开发之路,穷且益坚,不坠青云之志

引言 2023毕业季&#xff0c;距离笔者毕业已过2年有余。 互联网从业环境由盛转衰&#xff0c;互联网从业者数量剧增&#xff0c;市场竞争异常激烈&#xff0c;原本的利润空间被不断挤压&#xff0c;以至于很多开发者对互联网已经失去了信心与激情。 互联网的市场份额依旧是占…

vulntarget-j内网靶机write-up

文章目录 第一部分 获取边界服务器0x01 漏洞扫描0x02 漏洞利用第二部分 信息收集+代理0x01 连接工具0x02 进行信息收集0x03 sock代理设置第三部分 内网漫游0x01 通过代理获取服务器0x02 信息收集-获取账号信息0x03 上线CS0x04 远程访问免责声明摘抄第一部分 获取边界服务器 0x0…

JSP 在线药品管理系统用myeclipse定制开发sqlserver数据库网页模式java编程jdbc

一、源码特点 JAVA 在线药品管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 JSP 在线药品管理系统用myeclipse定制开发sqls 二、功能介绍 此次系统主要在JSP…

go context详解

文章目录 摘要1. context接口2. 实现context接口的类型2.1 emptyCtx2.2 valueCtx2.3 cancelCtx2.4 timerCtx 摘要 Context是go语言用于上下文管理的包&#xff0c;主要用于携程间的上下文管理&#xff0c;控制携程按时或者按时间取消执行。多个Context按树形或者链表的结果向前…

深圳阿里云代理商:阿里云服务器的可用区和地域选择有哪些考虑因素?如何优化性能?

阿里云服务器的可用区和地域选择有哪些考虑因素&#xff1f;如何优化性能&#xff1f;   选择阿里云服务器时&#xff0c;可用区和地域选择是一个非常关键的环节。本文将为您详细解析在这个过程中需要考虑的因素以及如何优化性能。   一、阿里云服务器的可用区和地域选择的…

Linux进程编程、fork函数范例详解 ( 5 ) -【Linux通信架构系列 】

系列文章目录 C技能系列 Linux通信架构系列 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程 期待你的关注哦&#xff01;&#xff01;&#xff01; 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the…

Spring高手之路6——Bean生命周期的扩展点:BeanPostProcessor

文章目录 1. 探索Spring的后置处理器&#xff08;BeanPostProcessor&#xff09;1.1 BeanPostProcessor的设计理念1.2 BeanPostProcessor的文档说明 2. BeanPostProcessor的使用2.1 BeanPostProcessor的基础使用示例2.2 利用BeanPostProcessor修改Bean的初始化结果的返回值2.3 …

【Linux工具】yum指令、vim的使用和修改信任白名单

【Linux工具】yum指令、vim的使用和修改信任白名单 目录 【Linux工具】yum指令、vim的使用和修改信任白名单软件包rzsz查看软件包 安装软件卸载软件vim的使用vim的三种模式vim的基本操作vim命令模式命令集vim末行模式命令集vim操作总结 简单vim配置修改信任白名单 作者&#xf…

第四章 进程同步

目录 一、进程同步、进程互斥 1.1 进程同步 1.2 进程互斥 二、信号量机制 2.1 整型信号量 2.2 记录型信号量 三、用信号量实现进程互斥、同步、前驱关系 3.1 信号量机制实现进程互斥 3.2 信号量机制实现进程同步 3.3 信号量机制实现前驱关系 四、生产者-消费者问题…

【MySQL】· 一文了解四大子查询

前言 ✨欢迎来到小K的MySQL专栏&#xff0c;本节将为大家带来MySQL标量/单行子查询、列子/表子查询的讲解✨ 目录 前言一、子查询概念二、标量/单行子查询、列子/表子查询三、总结 一、子查询概念 子查询指一个查询语句嵌套在另一个查询语句内部的查询&#xff0c;这个特性从My…

MySQL - 第7节 - MySQL内置函数

1.日期函数 1.1.常用的日期函数 常用的日期函数如下&#xff1a; 1.2.current_date函数 current_date函数用于获取当前的日期。如下&#xff1a; 1.3.current_time函数 current_time函数用于获取当前的时间。如下&#xff1a; 1.4.current_timestamp函数 current_timestamp函数…

计组:各硬件工作原理

目录 ​编辑程序指令执行流程 程序执行指令前 执行各指令的顺序 程序&#xff08;每一条&#xff09;指令执行具体步骤 以第一步为例&#xff1a; 取指令&#xff08;#1~#4&#xff09; 初&#xff1a;&#xff08;PC)0&#xff0c;指向第一条指令的存储地址 #1&#x…

计算机组成原理学习笔记(学习中)

计算机系统概论 1.1计算机基本组成 冯诺依曼计算机特点&#xff1a; 计算机由五大部件组成&#xff1a;控制器&#xff0c;运算器&#xff0c;存储器&#xff0c;输入设备&#xff0c;输出设备 指令和数据以同等地位存于存储器&#xff0c;可按地址寻访 指令和数据用二进制…

一文读懂CAN总线及通信协议

CAN总线的汽车 CAN概念 CAN是控制器域网 (Controller Area Network, CAN) 的简称&#xff0c;是由研发和生产汽车电子产品著称的德国BOSCH公司开发了的&#xff0c;并最终成为国际标准&#xff08;ISO11898&#xff09;&#xff0c;是ISO国际标准化的串行通信协议。是国际上应…