代码随想录算法训练营第60期第二十二天打卡

news2025/5/6 0:52:05

        大家好!我们今天来到了一个全新的章节,回溯算法,那究竟什么是回溯算法,我们应该如何理解回溯算法,以及回溯算法可以解决的题目,我们今天就来一探究竟。

第一部分 回溯算法理论基础

        其实我可以告诉大家的是回溯算法其实本质上就是暴力,其实并不是什么高效的算法,但是有一些类型的题目不用回溯算法就解决不了,其实我们后面的图论里面会有一道题叫做所有可达的路径,那里其实就是递归与回溯相辅相成,那里的dfs(深度优先搜索)本质上就是递归,其实回溯本质就是暴力枚举(穷举),我们的确可以通过一些剪枝的操作来尽可能减小时间复杂度,但其实我们还是改变不了回溯的本质是暴力,有一些问题能暴力搜素出来就不错了,所以我们不得不采用回溯的算法。

        那么回溯算法可以解决什么样的题目呢?首先我们可以解决以下几类问题:

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

       这些我们在后面都会有所涉及,大家只需要了解一下就可以了,还有回溯的模板,回溯其实是在一棵树上进行操作的,回溯法解决的都是在集合中递归查找子集,递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。

       那么提到递归的模板其实大家刷题刷多了逐渐有了自己的理解以后自然就会总结出属于自己的模板,我在这里简单提一下:

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

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

      先找到递归的终止条件,加下来是遍历搜索符合条件的节点,然后回溯,因为我们还会有下一次操作,递归与回溯两者不可分割,就比如说我要找3的全排列的所有情况,1后面可以有2,3,如果我找全了一种情况,我就必须回溯才能找全所有情况,这个大家要注意。好了那我们就开始尝试一下今天的回溯算法的题目了。

第一题对应力扣编号为77的题目组合

       我们来到今天的第一题,这个应该可以算是回溯算法的入门题目了,我们来看一下题目要求:

        题目的意思是给出两个数求出给定范围内的给定个数的组合数,这个如果不用回溯算法的话真的似乎没有其他的方法了,其实我给大家举一个例子,比如说1,2,3,4我要找出所有大小为2的组合,其实大家可以很轻松的写出代码,就是两层嵌套的for循环即可:

#include <iostream>
#include <vector>

using namespace std;
vector<int> vec = {1, 2, 3, 4};
int main()
{
	for (int i = 0; i < vec.size(); ++i)
	{
		for (int j = i + 1; j < vec.size(); ++j)
		{
			cout << vec[i] << " " << vec[j] << '\n'; 
		}
	}
	return 0;
}

     如果是找出所有大小为3的组合呢?大家也可以快速写出代码:

#include <iostream>
#include <vector>

using namespace std;
vector<int> vec = {1, 2, 3, 4};
int main()
{
	for (int i = 0; i < vec.size(); ++i)
	{
		for (int j = i + 1; j < vec.size(); ++j)
		{
			for (int k = j + 1; k < vec.size(); ++k)
			{
				cout << vec[i] << " " << vec[j] << " " << vec[k] << '\n';
			}
		}
	}
	return 0;
}

      其实我们可以看出,一旦组合的长度大了,我们的for嵌套很多就会导致超时,因为我们需要想一种方法,我们要先清楚一个问题其实我们的回溯算法本质是通过一棵树来实现的:

       但是大家还要时刻注意我们都要取不同的元素,比如说为什么取了2后面不会出现【2,1】这种情况就是考虑到了组合是无序的,【1,2】与【2,1】算作同一个,而且每一个元素只能去一次,自然也不会出现【2,2】的情况,大家了解了树形结构以后,接下来就是很重要的写代码环节,结合我们二叉树章节的递归三部曲,我们就可以尝试写出本题的代码:

class Solution {
private:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking(int n, int k, int startIndex)
    {
        //终止条件
        if (path.size() == k)
        {
            result.push_back(path);
            return;
        }
        //单层递归的逻辑
        for (int i = startIndex; i <= n; ++i)
        {
            path.push_back(i);
            backtracking(n, k, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combine(int n, int k) {
        path.clear();
        result.clear();
        backtracking(n, k, 1);
        return result;
    }
};

       我们来理解一下,其实如果初次接触回溯算法的话还是很难的,首先我们借助一个数组存储我们当前正在搜索到路径,当长度够了的时候我们就到了单次递归的终止条件,我们就把我们搜索到的路径添加到二维数组里这样我们就返回二维数组,这是我们题目要求我们返回的结果,大家尤其注意我中间的递归逻辑,我们一定不要忘记回溯,如果我们搜索到了【1,2】这条路只有回溯我们才可以搜索到【1,3】这条路,这就是回溯的意义,不要忘记任何的可行解,而且时刻注意我们参数的变化,注意我们起始的索引,保证不重复就可以了。

第二题对应力扣编号为216的题目组合总和III

       我们来到了今天的第二道题目,其实前面的那道题我们是可以做剪枝操作的,但是我们先不看我们先理解好回溯算法再去考虑如何优化,我们来看一下这道题目是什么意思:

       题目意思很好明白,其实比上一题给我们添加了一个限制条件而已,就是要求我找到的组合的和必须是给定值,我们应该如何考虑呢?其实我们还是得注意递归的终止条件以及回溯过程,如果说我们找到了一个符合要求的组合我们就添加进二维数组里面最后返回二维数组即可:

class Solution {
private:
    vector<int> path;
    vector<vector<int>> result;
    //解释一下我们递归函数的参数与返回值这不也是我们递归三部曲的第一步第一个参数targetSum是题目给的目标和
    //k表示要求的长度,sum存储的是当前的和,startIndex是起始的索引就是可以搜索的元素
    void backtracking(int targetSum, int k, 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);
            backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
            //然后回溯就是减去i然后再将当前元素退出数组
            sum -= i;
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum3(int k, int n) {
        result.clear();
        path.clear();
        backtracking(n, k, 0, 1);
        return result;
    }
};

      具体的过程我在代码注释里面解释的很清楚,大家多多思考,理解回溯不容易。

第三题对应力扣题号为17的题目电话号码的字母组合

      这是今天的最后一道题目,我们直接来看一下题目要求是什么:

       其实这里存在一个映射,就是一个数字对应几个字母,给出我们两个数字字符求出所有存在的字母的组合,我们应该如何思考?

class Solution {
private:
    const string letterMap[10] = {
        "", // 0
        "", // 1
        "abc", // 2
        "def", // 3
        "ghi", // 4
        "jkl", // 5
        "mno", // 6
        "pqrs", // 7
        "tuv", // 8
        "wxyz", // 9
    };
public:
    vector<string> result;
    string s;
    void backtracking(const string &digits, int index)
    {
        //递归的终止条件
        if (index == digits.size())
        {
            result.push_back(s);
            return;
        }
        //转换为整型数字(方便去寻找对应的字母)
        int digit = digits[index] - '0';
        string letters = letterMap[digit];      // 取数字对应的字符集
        //这样我们开始找对应的字母
        for (int i = 0; i < letters.size(); ++i)
        {
            s.push_back(letters[i]);
            backtracking(digits, index + 1);
            s.pop_back();
        }
    }
    vector<string> letterCombinations(string digits) {
        s.clear();
        result.clear();
        //如果给出的数字字符串是0就直接返回空数组就可以
        if (digits.size() == 0) return result;
        backtracking(digits, 0);
        return result;
    }
};

      这个题目有点抽象容易搞混,我们的映射就实现了数字与字母的关系,我们还需要去遍历当前要求的数字所对应的字母字符串在这期间我们才可以使用递归与回溯,一个个字母去找,找完第一个数字对应的再去找下一个直到达到了题目给出数字字符串的长度即可。

今日总结

      回溯算法其实是很重要的,我们在后面的图论会反复使用,大家理解好递归三部曲就好,搞明白我们回溯的对象是谁,具体题目具体分析就可以了。今天我们就到这里,我们下一次再见!

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

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

相关文章

自主机器人模拟系统

一、系统概述 本代码实现了一个基于Pygame的2D自主机器人模拟系统&#xff0c;具备以下核心功能&#xff1a; 双模式控制&#xff1a;支持手动控制&#xff08;WASD键&#xff09;和自动导航模式&#xff08;鼠标左键设定目标&#xff09; 智能路径规划&#xff1a;采用改进型…

基于QT的仿QQ音乐播放器

一、项目介绍 该项目是基于QT开发的⾳乐播放软件&#xff0c;界面友好&#xff0c;功能丰富&#xff0c;主要功能如下&#xff1a; 窗口hand部分&#xff1a; 点击最小化按钮&#xff0c;窗口最小化 点击最大化按钮&#xff0c;窗口最大化 点击关闭按钮&#xff0c;程序退出 …

腾讯研究院:《工业大模型应用报告》(文末附下载方式)

腾讯研究院发布的《工业大模型应用报告》是一份系统探讨大模型技术在工业领域落地实践的研究成果。该报告基于腾讯在人工智能、云计算及产业互联网的实践经验&#xff0c;结合国内外典型案例&#xff0c;深入分析了工业大模型的行业价值、关键技术、应用场景及未来趋势。报告指…

【DeepMLF】具有可学习标记的多模态语言模型,用于情感分析中的深度融合

这是一篇我完全看不懂的论文,写的好晦涩,适合唬人,所以在方法部分我以大白话为主 abstract 在多模态情感分析(MSA)中,多模态融合已经得到了广泛的研究,但融合深度和多模态容量分配的作用还没有得到充分的研究。在这项工作中,我们将融合深度、可扩展性和专用多模容量作…

uniapp如何获取安卓原生的Intent对象

通过第三方app唤起&#xff0c;并且获取第三方app唤起时携带的参数 因为应用a唤起应用b时&#xff0c;应用b第一时间就要拿到参数token&#xff0c;所以需要将获取参数的方法写在APP.vue中的onLaunch钩子里,如果其他地方要用可以选择vuex或者采用本地缓存。 uniapp中plus.run…

tinycudann安装过程加ubuntu18.04gcc版本的升级(成功版!!!!)

使用的是 Linux&#xff0c;安装以下软件包 sudo apt-get install build-essential git安装 CUDA 并将 CUDA 安装添加到您的 PATH。 例如&#xff0c;如果您有 CUDA 12.6.3&#xff0c;请将以下内容添加到您的/usr/local/~/.bashrcexport PATH"/usr/local/cuda-12.6.3/bi…

Android 实现一个隐私弹窗

效果图如下&#xff1a; 1. 设置同意、退出、点击用户协议、点击隐私协议的函数参数 2. 《用户协议》、《隐私政策》设置成可点击的&#xff0c;且颜色要区分出来 res/layout/dialog_privacy_policy.xml 文件 <?xml version"1.0" encoding"utf-8"?&…

[学成在线]22-自动部署项目

自动部署 实战流程 下边使用jenkins实现CI/CD的流程。 1、将代码使用Git托管 2、在jenkins创建任务&#xff0c;从Git拉取代码。 3、拉取代码后进行自动构建&#xff1a;测试、打包、部署。 首先将代码打成镜像包上传到docker私服。 自动创建容器、启动容器。 4、当有代…

Golang|使用函数作为参数和使用接口的联系

函数作为数据类型的一种&#xff0c;可以成为其他函数的参数。在 Go&#xff08;Golang&#xff09; 中&#xff0c;函数作为参数 和 接口&#xff08;interface&#xff09;&#xff0c;本质上都和抽象、灵活调用有关 —— 都是让代码更灵活、更可扩展的手段。不过它们各有侧重…

MATLAB技巧——norm和vecnorm两个函数讲解与辨析

在 MATLAB 中,norm 和 vecnorm 是两个用于计算向量或矩阵范数的函数,虽然它们的功能相似,但在使用场景和适用性上存在一些区别。本文将详细解释这两个函数的用途、功能以及如何选择合适的函数。 文章目录 norm函数用法范数类型vecnorm函数用法范数类型选择合适的函数示例对比…

ubuntu的libc 库被我 sudo apt-get --reinstall install libc6搞没了

我系统的libc 没了 今天为了运行一个开源的yuv 播放器&#xff0c;在运行的时候提醒 Inconsistency detected by ld.so: dl-call-libc-early-init.c: 37: _dl_call_libc_early_init: Assertion sym ! NULL failed!然后听从AI 的建议 当我去执行ls 时&#xff0c;系统提示 就这…

Ubuntu搭建Conda+Python开发环境

目录 一、环境说明 1、测试环境为ubuntu24.04.1 2、更新系统环境 3、安装wget工具 4、下载miniconda安装脚本 二、安装步骤 1、安装miniconda 2、source conda 3、验证版本 4、配置pip源 三、conda用法 1、常用指令 一、环境说明 1、测试环境为ubuntu24.04.1 2、更…

Python全流程开发实战:基于IMAP协议安全下载个人Gmail邮箱内所有PDF附件

在日常办公场景中&#xff0c;面对成百上千封携带PDF附件的邮件&#xff0c;手动逐一下载往往耗时耗力&#xff0c;成为效率瓶颈。如何通过代码实现“一键批量下载”&#xff1f;本文将以**“Gmail全量PDF附件下载工具”**开发为例&#xff0c;完整拆解从需求分析到落地交付的P…

Pytest-mark使用详解(跳过、标记、参数 化)

1.前言 在工作中我们经常使用pytest.mark.XXXX进行装饰器修饰&#xff0c;后面的XXX的不同&#xff0c;在pytest中有不同的作 用&#xff0c;其整体使用相对复杂&#xff0c;我们单独将其抽取出来做详细的讲解。 2.pytest.mark.skip()/skipif()跳过用例 import pytest #无条…

【浅尝Java】Java简介第一个Java程序(含JDK、JRE与JVM关系、javcdoc的使用)

&#x1f35e;自我激励&#xff1a;每天努力一点点&#xff0c;技术变化看得见 文章目录 Java语言概述Java是什么Java语言的重要性Java语言发展简史Java语言特性 第一个Java程序main方法示例运行Java程序JDK、JRE、JVM之间的关系注释基本规则注释规范 标识符关键字 Java语言概述…

项目三 - 任务2:创建笔记本电脑类(一爹多叔)

在本次实战中&#xff0c;我们通过Java的单根继承和多接口实现特性&#xff0c;设计了一个笔记本电脑类。首先创建了Computer抽象类&#xff0c;提供计算的抽象方法&#xff0c;模拟电脑的基本功能。接着定义了NetCard和USB两个接口&#xff0c;分别包含连接网络和USB设备的抽象…

Electron学习+打包

1. 什么是 Electron&#xff1f; Electron 是⼀个 跨平台桌⾯应⽤ 开发框架&#xff0c;开发者可以使⽤&#xff1a;HTML、CSS、JavaScript 等 Web 技术来构建桌⾯应⽤程序&#xff0c;它的本质是结合了 Chromium 和 Node.js &#xff0c;现在⼴泛⽤于桌⾯应 ⽤程序开发&a…

NumPy线性代数功能全解析:矩阵运算与方程求解实用指南

NumPy 是线性代数领域中高效的工具。它可以帮助完成矩阵运算和方程求解。本文将介绍 NumPy 中用于线性代数的常用函数。 矩阵乘法 矩阵乘法会根据两个矩阵生成一个新矩阵。具体做法是将第一个矩阵的每一行与第二个矩阵的每一列相乘&#xff0c;并将乘积相加&#xff0c;得到新…

《RabbitMQ 全面解析:从原理到实战的高性能消息队列指南》

一、RabbitMQ 核心原理与架构 1. 核心组件与工作流程 RabbitMQ 基于 AMQP 协议&#xff0c;核心组件包括 生产者&#xff08;Producer&#xff09;、交换机&#xff08;Exchange&#xff09;、队列&#xff08;Queue&#xff09; 和 消费者&#xff08;Consumer&#xff09;。…

Android Framework学习二:Activity创建及View绘制流程

文章目录 Window绘制流程Window Manager Service&#xff08;WMS&#xff09;SurfaceSurfaceFlinger 安卓View层次结构ActivityPhoneWindowActivity与PhoneWindow两者之间的关系ViewRootImplDecorViewDecorView 的作用DecorView 的结构总结 Activity创建流程View invalidate调用…