【算法】面试题 - 回溯算法解题套路框架

news2024/11/15 19:58:19

回溯算法解题套路框架

  • 前言
    • 回溯算法的框架
  • 排列(元素无重不可复选)
    • 46. 全排列
      • 解析
  • 子集(元素无重不可复选)
    • 78. 子集
      • 解析
  • 组合(元素无重不可复选)
    • 77. 组合
      • 解析
  • 子集/组合(元素可重不可复选)
    • 90. 子集 II
      • 解析
  • 面试题
    • 31. 下一个排列

前言

回溯算法的框架

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    
    for 选择 in 选择列表:
        # 做选择
        排除不合法的选择
        路径.add(选择)
        backtrack(路径, 选择列表)
        # 撤销选择
        路径.remove(选择)

其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」。
什么叫做选择和撤销选择呢,这个框架的底层原理是什么呢?下面我们就通过「46. 全排列」这个问题来解开之前的疑惑,详细探究一下其中的奥妙!

排列(元素无重不可复选)

46. 全排列

问题描述
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例1

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例2

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例3

输入:nums = [1]
输出:[[1]]

代码

//存储结果
List<List<Integer>> result = new ArrayList<>();

public List<List<Integer>> permute(int[] nums) {
    //记录路径
    LinkedList<Integer> track = new LinkedList<>();
    backtrack(nums, track);
    return result;
}

void backtrack(int[] nums, LinkedList<Integer> track) {
    //到达叶子节点,将路径存入result
    if (track.size() == nums.length) {
        result.add(new LinkedList<>(track));
        return;
    }

    for (int i = 0; i < nums.length; i++) {
        //排除不合法的选择
        if (track.contains(nums[i])) {
            continue;
        }
        //做选择
        track.add(nums[i]);
        //进入下一层
        backtrack(nums, track);
        //取消选择
        track.removeLast();
    }
}

解析

我们在高中的时候就做过排列组合的数学题,我们也知道 n 个不重复的数,全排列共有 n! 个。那么我们当时是怎么穷举全排列的呢?

比方说给三个数 [1,2,3],你肯定不会无规律地乱穷举,一般是这样:

先固定第一位为 1,然后第二位可以是 2,那么第三位只能是 3;然后可以把第二位变成 3,第三位就只能是 2 了;然后就只能变化第一位,变成 2,然后再穷举后两位……

其实这就是回溯算法,我们高中无师自通就会用,或者有的同学直接画出如下这棵回溯树:
在这里插入图片描述
只要从根遍历这棵树,记录路径上的数字,其实就是所有的全排列。我们不妨把这棵树称为回溯算法的「决策树」。
为啥说这是决策树呢,因为你在每个节点上其实都在做决策。比如说你站在下图的红色节点上:
在这里插入图片描述
你现在就在做决策,可以选择 1 那条树枝,也可以选择 3 那条树枝。为啥只能在 1 和 3 之中选择呢?因为 2 这个树枝在你身后,这个选择你之前做过了,而全排列是不允许重复使用数字的。

现在可以解答开头的几个名词:[2] 就是「路径」,记录你已经做过的选择;[1,3] 就是「选择列表」,表示你当前可以做出的选择;「结束条件」就是遍历到树的底层叶子节点,这里也就是选择列表为空的时候
如果明白了这几个名词,可以把「路径」和「选择」列表作为决策树上每个节点的属性,比如下图列出了几个蓝色节点的属性:
在这里插入图片描述
我们定义的 backtrack 函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层叶子节点,其「路径」就是一个全排列。

「路径」和「选择」是每个节点的属性,函数在树上游走要正确处理节点的属性,那么就要在这两个特殊时间点搞点动作:
在这里插入图片描述

子集(元素无重不可复选)

78. 子集

问题描述
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例1

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例2

输入:nums = [0]
输出:[[],[0]]

代码

List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();

// 主函数
public List<List<Integer>> subsets(int[] nums) {
    backtrack(nums, 0);
    return res;
}

// 回溯算法核心函数,遍历子集问题的回溯树
void backtrack(int[] nums, int start) {

    // 前序位置,每个节点的值都是一个子集
    res.add(new LinkedList<>(track));

    // 回溯算法标准框架
    for (int i = start; i < nums.length; i++) {
        // 做选择
        track.addLast(nums[i]);
        // 通过 start 参数控制树枝的遍历,避免产生重复的子集
        backtrack(nums, i + 1);
        // 撤销选择
        track.removeLast();
    }
}

解析

使用 start 参数控制树枝的生长避免产生重复的子集,用 track 记录根节点到每个节点的路径的值,同时在前序位置把每个节点的路径值收集起来,完成回溯树的遍历就收集了所有子集:
在这里插入图片描述
最后,backtrack 函数开头看似没有 base case,会不会进入无限递归?
其实不会的,当 start == nums.length 时,叶子节点的值会被装入 res,但 for 循环不会执行,也就结束了递归。

组合(元素无重不可复选)

如果你能够成功的生成所有无重子集,那么你稍微改改代码就能生成所有无重组合了。
你比如说,让你在 nums = [1,2,3] 中拿 2 个元素形成所有的组合,你怎么做?
稍微想想就会发现,大小为 2 的所有组合,不就是所有大小为 2 的子集嘛。
所以我说组合和子集是一样的:大小为 k 的组合就是大小为 k 的子集。

77. 组合

问题描述
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例

输入:n = 1, k = 1
输出:[[1]]

代码

List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();

// 主函数
public List<List<Integer>> combine(int n, int k) {
    backtrack(1, n, k);
    return res;
}

void backtrack(int start, int n, int k) {
    // base case
    if (k == track.size()) {
        // 遍历到了第 k 层,收集当前节点的值
        res.add(new LinkedList<>(track));
        return;
    }
    
    // 回溯算法标准框架
    for (int i = start; i <= n; i++) {
        // 选择
        track.addLast(i);
        // 通过 start 参数控制树枝的遍历,避免产生重复的子集
        backtrack(i + 1, n, k);
        // 撤销选择
        track.removeLast();
    }
}

解析

这是标准的组合问题,但我给你翻译一下就变成子集问题了:
给你输入一个数组 nums = [1,2…,n] 和一个正整数 k,请你生成所有大小为 k 的子集。
还是以 nums = [1,2,3] 为例,刚才让你求所有子集,就是把所有节点的值都收集起来;现在你只需要把第 2 层(根节点视为第 0 层)的节点收集起来,就是大小为 2 的所有组合:
在这里插入图片描述

子集/组合(元素可重不可复选)

90. 子集 II

问题描述
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
输入:nums = [0]
输出:[[],[0]]

代码

List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();

public List<List<Integer>> subsetsWithDup(int[] nums) {
    // 先排序,让相同的元素靠在一起
    Arrays.sort(nums);
    backtrack(nums, 0);
    return res;
}

void backtrack(int[] nums, int start) {
    // 前序位置,每个节点的值都是一个子集
    res.add(new LinkedList<>(track));
    
    for (int i = start; i < nums.length; i++) {
        // 剪枝逻辑,值相同的相邻树枝,只遍历第一条
        if (i > start && nums[i] == nums[i - 1]) {
            continue;
        }
        track.addLast(nums[i]);
        backtrack(nums, i + 1);
        track.removeLast();
    }
}

解析

按照之前的思路画出子集的树形结构,显然,两条值相同的相邻树枝会产生重复:
在这里插入图片描述
所以我们需要进行剪枝,如果一个节点有多条值相同的树枝相邻,则只遍历第一条,剩下的都剪掉,不要去遍历:
在这里插入图片描述
体现在代码上,需要先进行排序,让相同的元素靠在一起,如果发现 nums[i] == nums[i-1],则跳过:

这段代码和之前标准的子集问题的代码几乎相同,就是添加了排序和剪枝的逻辑。
至于为什么要这样剪枝,结合前面的图应该也很容易理解,这样带重复元素的子集问题也解决了。

面试题

31. 下一个排列

问题描述
实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须原地修改,只允许使用额外常数空间。

示例

1,2,31,3,2
3,2,11,2,3
1,1,51,5,1

代码
求下一个全排列,可分为两种情况:
1.例如像 5 4 3 2 1这样的序列,已经是最大的排列,即每个位置上的数非递增,这时只需要翻转整个序列即可
2.例如像 1 3 5 4 2这样的序列,要从后往前找到第一个比后面一位小的元素的位置,即第二个位置的3,然后与其后第一个比它大的元素交换位置,得到 1 4 5 3 2,再将 5 3 2翻转得到 1 4 2 3 5即可

    // 1 3 5 4 2
    static void nextPermutation(int[] nums) {
        int n = nums.length;
        if (n <= 1) {
            return;
        }

        //找到i(num[i]<num[i+1]) 第一个非降序的下标,i后面的元素都是降序的,证明已经是最大值
        int i = n - 2; //1
        while (i >= 0 && nums[i] >= nums[i + 1]) {
            i--;
        }

        //数字整体降序,已经是最大值,这时候只需要通过反转就可以得到最小值
        if (i < 0) {
            reverse(nums, 0, n - 1);
        }

        //从(i,n-1)范围内遍历找到比nums[i]大的数字,倒着遍历(因为i至n-1的数字整体降序,倒着遍历找到第一个比num[i]大的数字)
        int j = n - 1; //3
        while (i < j && nums[j] <= nums[i]) {
            j--;
        }

        //i和j交换
        swap(nums, i, j); //i:1 j:3 结果 1 4 5 3 2 
        reverse(nums, i + 1, n - 1); //为保证数字是最小的,所以i+1至n-1的反转 结果 1 4 2 3 5
    }

    static void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

    static void reverse(int[] nums, int start, int end) {
        while (start < end) {
            swap(nums, start++, end--);
        }
    }

来源(算法小抄):https://labuladong.gitee.io/algo/1/8/
下一个排列(视频讲解):https://www.bilibili.com/video/BV1Rz4y1Z7hx

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

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

相关文章

免费PDF转Word?有这几个网站就够了

如果您想使用 Word 文档&#xff0c;您可能需要将PDF 转换为 Word&#xff0c;以便您可以随意使用该文档。将 PDF 转换为 Word 的过程需要一个好的 PDF 转换器。在本文中&#xff0c;您将探索可用的 5个免费转换器&#xff0c;其中包括 奇客PDF 和PDF2Go。 最好的 6 个 PDF 转 …

【unity笔记】图解Vector3.SignedAngle()方法的返回值

首先看一下官方文档的说明&#xff1a; public static float SignedAngle (Vector3 from, Vector3 to, Vector3 axis); from测量角度差的源向量。to测量角度差的目标向量。axis一个向量&#xff0c;其他向量将绕其旋转。返回 from 与 to 之间的有符号角度&#xff08;以度为单…

CodeQL 源代码漏洞扫描

目录 1、下载配置 codeql 1.1 配置 codeql 1.2 配置 maven 2、测试 codeql 漏洞检测 2.1 构建 codeql 查询数据库 2.2 漏洞检测 测试环境&#xff1a;centos7 jdk11 maven 1、下载配置 codeql 1.1 配置 codeql 下载安装 codeql-cli: https://github.com/github/code…

大数据系列——什么是Flink?Flink有什么用途?

目录 一、基本概念 批与流 数据可以作为无界流或有界流处理 二、什么是Flink&#xff1f; 三、Flink有什么用途&#xff1f; 四、适用场景 五、flink事件驱动 六、flink拥有分层API flink sql 七、fllink企业级使用 一、基本概念 批与流 批处理的特点是有界、持久、大…

被误认为是外国人开发的4款软件,功能强大到离谱,且用且珍惜

国外的月亮不一定比国内圆&#xff0c;随着国内互联网飞速发展&#xff0c;国内研发出许多实用又良心的软件&#xff0c;由于偏见&#xff0c;功能强大的它们却被误认为是外国佬研发的。 1、Foxit PDF用系统自带的Adobe实在难用&#xff0c;Foxit这款PDF阅读器实在太强大&#…

unity使用RenderTexture可以渲染粒子特效

一&#xff0c;使用UIRawImage,创建材质球&#xff0c;把Shader给材质球&#xff0c;放到RawImage的Material上&#xff0c; // Upgrade NOTE: replaced mul(UNITY_MATRIX_MVP,*) with UnityObjectToClipPos(*)Shader "UI/Default No-Alpha" {Properties{[PerRende…

基于文本和图像的网络舆情分析方法研究

基于文本和图像的网络舆情分析方法研究 一、舆情分析技术 &#xff08;1&#xff09;舆情数据采集与提取技术&#xff1b; &#xff08;2&#xff09;自动文摘技术&#xff1b; &#xff08;3&#xff09;事件发现与追踪技术&#xff1b; &#xff08;4&#xff09;舆情情感分…

【虚幻引擎UE】UE5 模型描边的三个方法

一、后期处理法 1、创建描边材质&#xff0c;方法很多种&#xff0c;主要有设置深度、法线描边等 可以参考现有文章制作或直接下载材质资源使用。 参考文章&#xff1a; 1、【UE4】几种后处理描边的方法&#xff0c;效果及效率 2、UE4之物体描边 3、【UE4_001】后期处理轮廓…

第002课 - 项目整体效果展示

文章目录 基础篇高级篇流量控制:alibaba sentinel注册中心链路追踪高可用集群篇CICD这个章节是进行项目效果的演示。 基础篇 第一个就是基础篇。 这是我们的后台管理系统。 围绕电商的管理系统做一个整套的增删改查逻辑。 这个商品系统都会教给大家来编写的。 这个是使用前…

网站报错:PHP Fatal error: Allowed memory size of 134217728 bytes exhausted的处理方法

原因分析 内存已耗尽&#xff0c;这关系到PHP的memory_limit的设置问题&#xff0c;根据自己的需要及参考本机的内存大小修改php内存限制。 解决方案 1、修改php.ini &#xff08;改配置&#xff09; memory_limit 128 这种方法需要重启服务器&#xff0c;很显然&#xff0c…

向Linux内核添加驱动的步骤详解

1、获取驱动源码 (1)驱动源码一般都是从设备厂商处获取&#xff1b; (2)设备厂商给的驱动源码大体上是没有问题的&#xff0c;能加载但是效果不一定好&#xff0c;需要根据自己的板子进行适配&#xff1b; 2、驱动在内核中的两种形式 (1)直接编译进内核&#xff1a;内核启动时自…

ubuntu下编译opencv

目录 1. 下载opencv和opencv-contrib 2. 安装依赖 3. cmake 4. make 5. 安装 6. 配置opencv的路径 7. 测试 后续 1. 下载opencv和opencv-contrib https://github.com/opencv/opencv/archive/refs/tags/4.6.0.zip https://github.com/opencv/opencv_contrib/archive/re…

python两种方式实现读写航拍影像JPG图片的GPS坐标

写入坐标效果 读取坐标效果 1、写入JPG坐标数据 1.1、准备数据 gps坐标文件 图片 可以查看它的属性中目前并没有坐标信息 1.2、执行脚本 第一种方法(piexif) writegps2jpg_piexif.py import csv,os import

KubeEdge云原生边缘计算公开课04——云原生边缘计算学术研究现状与趋势

KubeEdge云原生边缘计算公开课04——云原生边缘计算学术研究现状与趋势Ding Yin & 徐飞&#xff1a;KubeEdge架构与技术解读1. 边缘计算的形态定义与关键挑战2. 云原生边缘计算的优势与挑战3. KubeEdge核心架构4. KubeEdge关键技术5. KubeEdge社区介绍Ding Yin & 徐飞&…

JavaSE笔记——流式编程

文章目录前言一、从外部迭代到内部迭代二、实现机制三、常用的流操作1.collect(toList())2.map3.filter4.flatMap5.max和min6.reduce四、多次调用流操作五、高阶函数总结前言 流是一系列与特定存储机制无关的元素——实际上&#xff0c;流并没有 “存储” 之说。利用流&#x…

火山引擎 DataTester:如何做 A/B 实验的假设检验作者:字节跳动数据平台

A/B 实验的核心统计学理论是&#xff08;双样本&#xff09;假设检验&#xff0c;是用来判断样本与样本、样本与总体的差异是由 抽样误差 引起还是 本质差别 造成的一种统计推断方法。 假设检验&#xff0c;顾名思义&#xff0c;是一种对自己做出的假设进行数据验证的过程。通…

STM32CUBEMX_SDIO和FATFS_读写SD卡

STM32CUBEMX_SDIO和FATFS_读写SD卡 简述 FATFS是一个完全免费开源&#xff0c;专为小型嵌入式系统设计的FAT&#xff08;File Allocation Table&#xff09;文件系统模块。FATFS的编写遵循ANSI C&#xff0c;并且完全与磁盘I/O层分开。支持FAT12/FAT16/FAT32&#xff0c;支持多…

Django入门

Django 中文官网&#xff1a;初识 Django | Django 文档 | Django (djangoproject.com) Django 是一个由 Python 编写的一个开放源代码的 Web 应用框架。 使用 Django&#xff0c;只要很少的代码&#xff0c;Python 的程序开发人员就可以轻松地完成一个正式网站所需要的大部…

Python 学习笔记001-发布

Python 学习笔记001-发布Python如何发布为EXE文件发给别人装X0 我的开发环境Step 1 安装PyInstaller包Step2 打包Python文件Step 3 运行Python程序Step 4 最后附上Atm.py的代码Python如何发布为EXE文件发给别人装X 0 我的开发环境 Python : 3.10 PyCharm:2022.03 社区版 Ste…

VIAVI唯亚威光纤高分辨率多模 OTDR 测试方案

VIAVI Solutions 高分辨率多模 OTDR 测试方案设计用于飞机、宇宙飞船、潜艇和舰船中部署的超短多模光纤的特性分析和故障定位 高分辨率多模 OTDR 测试方案是业界紧凑、轻巧的便携装置。它的用户界面经过专门设计&#xff0c;简化了 OTDR 测试和结果读取。 特点 紧凑、轻巧、现…