算法19天|回溯算法:理论基础、组合、组合总和Ⅲ、电话号码的字母组合

今日总结:

第一天学回溯,还是有点不太熟悉,需要重复思考每层递归的逻辑以及输入值

电话号码的字母组合(需要重新看):

(1)需要明白递归的是数字字符串中的数字"245678"

(2)通过每层的数字,在设置好的字符串数组中找到数字对应的字符串

(3)通过遍历字符串中的字符 ,记录找到的不同组合

理论基础:

1、什么是回溯算法:

回溯算法也叫做回溯搜索法,是一种搜索的方法。

回溯是递归的副产品,只要有递归就会有回溯

2、回溯算法的效率

回溯法不是什么高效的算法,回溯法的本质是穷举,穷举所有可能,选出所需要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改变不了回溯法就是穷举的本质。

3、回溯法解决的问题

1、组合问题:N个数里边按一定规则找出k个数的集合
2、切割问题:一个字符串按一定规则有几种切割方式
3、子集问题:一个N个数的集合里有多少符合条件的子集
4、排列问题:N个数按照一定的规则全排列,有几种排列方式
5、棋盘问题:N皇后,解数独等

4、组合、排列

组合:不强调元素的顺序,只要该元素存在就行

排列:强调元素的顺序

5、理解回溯算法:

回溯法解决的问题都可以抽象成树形结构。

回溯法解决的问题都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。

是递归就要有终止条件:所以必然是一棵高度有限的树(N叉树)

6、回溯算法模板

(1)回溯函数返回值以及参数

回溯算法函数名字:定义为backtracking

回溯算法函数返回值:一般为void

回溯算法的参数:根据写函数过程中缺什么补什么

cpp 复制代码
void backtracking(参数)

(2)回溯函数的终止条件

回溯算法的本质可以抽象成树状结构,所以肯定有终止条件,一般终止条件为搜索到了叶子节点,此时一般也就找到了满足条件的一条答案,将答案存储到全局变量中,结束本层递归

cpp 复制代码
if(终止条件)
{
    存放结果;
    return ;
}

(3)回溯的遍历过程

回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成了树的深度

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

组合

题目链接:77. 组合 - 力扣(LeetCode)

整体思路:

与二叉树不同,这种类型的题,每一层相当于有n种选择,所以在单层递归的时候需要将n种选择全部遍历(for循环)

1、确定返回值、输入值、全局变量

返回值:无

输入值:集合数n,递归次数k,当前递归从什么开始cur

全局变量:全部的路径,当前的路径

cpp 复制代码
    //回溯算法:
    //1、确定递归的参数、返回值、全局变量
    //返回值:无
    //输入值:集合n,次数k,当前循环开始位置cur
    //全局变量:总路径、单次路径
    vector<vector<int>>res;
    vector<int>patch;
    void backtracking(int n,int k , int cur)

2、确定递归停止逻辑:

遇到叶子节点(当前已经递归了k层),开始记录结果

cpp 复制代码
        //确定终止条件:
        //当递归到叶子节点:patch.size==k,记录结果并返回
        if(patch.size()==k)
        {
            res.push_back(patch);
            return;
        }

3、确定单层递归逻辑

先遍历所有可能的节点,然后去递归下一层,下一层开始的位置是当前层的下一个数

遍历之后,需要将当前层的存储路径之删除进行回溯

cpp 复制代码
        //确定单层回溯逻辑:
        //当前层开始遍历一层的节点
        for(int i=cur;i<=n;i++)
        {
            //将节点加入到路径中
            patch.push_back(i);
            //递归下一层
            backtracking(n,k,i+1);//开始位置是i+1
            //递归完后需要回溯
            patch.pop_back();
        }

整体代码:

cpp 复制代码
class Solution {
public:

    //回溯算法:
    //1、确定递归的参数、返回值、全局变量
    //返回值:无
    //输入值:集合n,次数k,当前循环开始位置cur
    //全局变量:总路径、单次路径
    vector<vector<int>>res;
    vector<int>patch;
    void backtracking(int n,int k , int cur)
    {
        //确定终止条件:
        //当递归到叶子节点:patch.size==k,记录结果并返回
        if(patch.size()==k)
        {
            res.push_back(patch);
            return;
        }
        //确定单层回溯逻辑:
        //当前层开始遍历一层的节点
        for(int i=cur;i<=n;i++)
        {
            //将节点加入到路径中
            patch.push_back(i);
            //递归下一层
            backtracking(n,k,i+1);//开始位置是i+1
            //递归完后需要回溯
            patch.pop_back();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backtracking(n,k,1);
        return res;
        
    }
};

组合总和Ⅲ

题目链接:216. 组合总和 III - 力扣(LeetCode)

整体思路:

与上一个题目类似

1、确定全局参数、返回值、输入值

全局参数:全部路径、单次路径

返回值:无

输入值:目标和n、次数k、当前层开始数cur、当前层和sum

cpp 复制代码
    //使用回溯递归
    //1、确定全局参数、返回值、输入值
    //全局参数:全部路径、单层路径
    vector<vector<int>>res;
    vector<int>patch;
    //返回值:无
    //输入值:和n、数k,当前值cur,当前和sum
    void backtracking(int n,int k,int cur,int sum)

2、确定终止条件

当路径中的值为次数时表示可以记录了

需要判断和是不是为n

可以进行剪枝:如果当前已经大于n,可以直接return

cpp 复制代码
        //确定停止条件:当前递归次数已经为k次
        //应该可以有剪枝,次数没到k就已经超过了n
        if(patch.size()<=k&&sum >n)
        {
            return ;
        }
        else if(patch.size()==k)
        {
            //如果k个数之和sum==n,就记录
            if(sum==n)
            {
                res.push_back(patch);
            }
            return ;
        }

3、确定单层递归逻辑

首先需要遍历当前层:当前层开始位置为i=cur

因为只能使用1-9数字,需要递归k次最后不能大于数字9,所以当前层数i、数字9、还需要递归几层之间存在一个关系:i<=9-(k-patch.size())+1

存储当前路径值、记录路径和

递归下一层,下一层开始位置为当前位置i+1(因为是组合)

回溯:删除路径最后一位、删除和中的最后一位路径值

cpp 复制代码
        //确定单层递归逻辑
        //首先遍历当前层
        //当前层的i需要满足:1、是传入当前层cur开始,2、需要确保最后一层不能大于9,也就是当前层i<=最大数9-当前存储的数的数量patch.size()与目标k的差
        for(int i=cur;i<=9-(k-patch.size())+1;i++)
        {
            //记录当前层
            patch.push_back(i);
            sum +=i;
            //递归下一层
            backtracking(n,k,i+1,sum);
            //回溯
            sum -=patch[patch.size()-1];
            patch.pop_back();
        }

整体代码:

cpp 复制代码
class Solution {
public:

    //使用回溯递归
    //1、确定全局参数、返回值、输入值
    //全局参数:全部路径、单层路径
    vector<vector<int>>res;
    vector<int>patch;
    //返回值:无
    //输入值:和n、数k,当前值cur,当前和sum
    void backtracking(int n,int k,int cur,int sum)
    {
        //确定停止条件:当前递归次数已经为k次
        //应该可以有剪枝,次数没到k就已经超过了n
        if(patch.size()<=k&&sum >n)
        {
            return ;
        }
        else if(patch.size()==k)
        {
            //如果k个数之和sum==n,就记录
            if(sum==n)
            {
                res.push_back(patch);
            }
            return ;
        }

        //确定单层递归逻辑
        //首先遍历当前层
        //当前层的i需要满足:1、是传入当前层cur开始,2、需要确保最后一层不能大于9,也就是当前层i<=最大数9-当前存储的数的数量patch.size()与目标k的差
        for(int i=cur;i<=9-(k-patch.size())+1;i++)
        {
            //记录当前层
            patch.push_back(i);
            sum +=i;
            //递归下一层
            backtracking(n,k,i+1,sum);
            //回溯
            sum -=patch[patch.size()-1];
            patch.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(n,k,1,0);
        return res;
        
    }
};

电话号码的字母组合

题目链接:17. 电话号码的字母组合 - 力扣(LeetCode)

整体思路:

难点:

1、数字2-9,每个数字都代表一个字母组合,当拿到一个数字要遍历其代表的所有字母,所以一个数字其实就对应一个字符串

2、给定的是随机的数字字符串,需要将字符串中的数字进行提取,然后将这个数字所对应的字符串进行遍历

所以,可以创建一个映射关系,具体就是一个字符串数组,数组下标表示数字,数组内容表示数字对应的映射字母

在遍历的时候将给定的随机数字字符串进行字符串的字符转数字,填入到映射字符串数组中,寻找对应的遍历,注意数字从2开始,数组从0开始

1、建立数字与字母字符串的映射

cpp 复制代码
    //创建一个数组,0表示空,1表示数字2
    string num_map_abc[8]={
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz"
    };

2、确定全局、输入、返回

全局:结果vector、单次返回结果string

输入:数字字符串,递归的层数

返回:无返回值

3、确定递归的终止条件

当递归的层数与数字字符串的数量一致后,记录返回

4、确定单层递归逻辑

(1)通过for循环去遍历当前层的元素

digitscur表示数字字符串中当前层的字符;

digitscur-'2'表示数字字符串中当前层的字符转换为数字

num_map_abcdigits\[cur-'2']表示数字字符串当前层的数字在字符串数组中对应的字符串

num_map_abcdigits\[cur-'2'].size表示数字字符串当前层的数字在字符串数字中对应的字符串元素个数

for(int i=0;i<num_map_abcdigits\[cur-'2'].size();i++)表示遍历当前层的字符串

cpp 复制代码
        for(int i=0;i<num_map_abc[digits[cur]-'2'].size();i++)

(2)在循环中记录当前值

cpp 复制代码
            //记录当前值
            s += num_map_abc[digits[cur]-'2'][i];

(3)进行下一层的递归

cpp 复制代码
            backtracking(digits,cur+1);

(4)进行回溯

cpp 复制代码
            s.pop_back();

整体代码:

cpp 复制代码
class Solution {
public:

    //创建一个数组,0表示空,1表示数字2
    string num_map_abc[8]={
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz"
    };

    //进行回溯
    //1、确定全局、输入、返回
    //全局:结果、单次结果
    vector<string>res;
    string s;
    //输入:digits,当前递归位置
    //返回:无
    void backtracking (string digits,int cur)
    {
        //2、确定停止条件
        //如果遍历完了digits中的所有元素
        if(cur==digits.size())
        {
            //记录值,返回
            res.push_back(s);
            return ;
        }
        //确定单层递归逻辑
        for(int i=0;i<num_map_abc[digits[cur]-'2'].size();i++)
        {
            //记录当前值
            s += num_map_abc[digits[cur]-'2'][i];
            backtracking(digits,cur+1);
            s.pop_back();
        }
    }
    vector<string> letterCombinations(string digits) {
        if (digits.size()==0)
            return res;
        backtracking(digits,0);
        return res;
        
    }
};
相关推荐
江畔柳前堤8 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore9 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考10 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX11 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面12 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号12 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽12 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI12 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者14 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者14 小时前
【征程6】校准量化中HistogramObserver解析
算法