LeetCode 847. Shortest Path Visiting All Nodes【状态压缩,BFS;动态规划,最短路】2200

news2025/1/12 19:05:02

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

存在一个由 n 个节点组成的无向连通图,图中的节点按从 0 到 n - 1 编号。

给你一个数组 graph 表示这个图。其中,graph[i] 是一个列表,由所有与节点 i 直接相连的节点组成。

返回能够访问所有节点的最短路径的长度。你可以在任一节点开始和停止,也可以多次重访节点,并且可以重用边。

示例 1:

输入:graph = [[1,2,3],[0],[0],[0]]
输出:4
解释:一种可能的路径为 [1,0,2,0,3]

示例 2:

输入:graph = [[1],[0,2,4],[1,3,4],[2],[1,2]]
输出:4
解释:一种可能的路径为 [0,1,4,2,3]

提示:

  • n == graph.length
  • 1 <= n <= 12
  • 0 <= graph[i].length < n
  • graph[i] 不包含 i
  • 如果 graph[a] 包含 b ,那么 graph[b] 也包含 a
  • 输入的图总是连通图

解法1 状态压缩 + 广度优先搜索

由于题目需要我们求出「访问所有节点的最短路径的长度」,并且图中每一条边的长度均为 1 1 1 ,因此我们可以考虑使用广度优先搜索的方法求出最短路径

在常规的广度优先搜索中,我们会在队列中存储节点的编号。对于本题而言,最短路径的前提是「访问了所有节点」,因此除了记录节点的编号以外,我们还需要记录每一个节点的经过情况。因此,我们使用三元组 ( u , m a s k , d i s t ) (u, mask,dist) (u,mask,dist) 表示队列中的每一个元素,其中:

  • u u u 表示当前位于的节点编号;
  • m a s k mask mask 是一个长度为 n n n 的二进制数,表示每一个节点是否经过。如果 m a s k mask mask 的第 i i i 位是 1 1 1 ,则表示节点 i i i 已经过,否则表示节点 i i i 未经过
  • d i s t dist dist 表示到当前节点为止经过的路径长度。

这样一来,我们使用该三元组进行广度优先搜索,即可解决本题。初始时,我们将所有的 ( i , 2 i , 0 ) (i,2^i,0) (i,2i,0) 放入队列,表示可以从任一节点开始。在搜索的过程中,如果当前三元组中的 m a s k mask mask 包含 n n n 1 1 1(即 mask = 2 n − 1 \textit{mask} = 2^n - 1 mask=2n1 ),那么我们就可以返回 d i s t dist dist 作为答案。

细节:为了保证广度优先搜索时间复杂度的正确性,即同一个节点 u u u 以及节点的经过情况 m a s k mask mask 只被搜索到一次,我们可以使用数组或者哈希表记录 ( u , m a s k ) (u,mask) (u,mask) 是否已经被搜索过,防止无效的重复搜索。

class Solution {
public:
    int shortestPathLength(vector<vector<int>>& g) {
        int n = g.size();
        queue<tuple<int, int, int>> q;
        vector<vector<bool>> vis(n, vector<bool>(1 << n)); // [u,mask],避免重复遍历
        for (int i = 0; i < n; ++i) {
            q.emplace(i, 1 << i, 0);
            vis[i][1 << i] = true;
        }
        int ans = 0;
        while (!q.empty()) {
            auto [u, mask, dist] = q.front();
            q.pop();
            if (mask == (1 << n) - 1) {
                ans = dist;
                break;
            }
            // 搜索相邻的节点
            for (int v : g[u]) {
                // 将mask的第v位置1
                int maskV = mask | (1 << v);
                if (!vis[v][maskV]) {
                    q.emplace(v, maskV, dist + 1);
                    vis[v][maskV] = true;
                }
            }
        }
        return ans;
    }
};

复杂度分析:

  • 时间复杂度: O ( n 2 ⋅ 2 n ) O(n^2 \cdot 2^n) O(n22n) 。常规的广度优先搜索的时间复杂度为 O ( n + m ) O(n+m) O(n+m) ,其中 n n n m m m 分别表示图的节点数和边数。本题中引入了 m a s k mask mask 这一维度,其取值范围为 [ 0 , 2 n ) [0, 2^n) [0,2n) ,因此可以看成是进行了 2 n 2^n 2n 次常规的广度优先搜索。由于 m m m 的范围没有显式给出,在最坏情况下为完全图,有 O ( n 2 ) = m O(n^2)=m O(n2)=m ,因此总时间复杂度为 O ( n 2 ⋅ 2 n ) O(n^2 \cdot 2^n) O(n22n)
  • 空间复杂度: O ( n ⋅ 2 n ) O(n \cdot 2^n) O(n2n) ,即为队列需要使用的空间。

解法2 预处理点对间最短路 + 状态压缩动态规划

由于题目中给定的图是连通图,那么我们可以计算出任意两个节点之间 u , v u, v u,v 间的最短距离,记为 d ( u , v ) d(u,v) d(u,v) 。这样一来,我们就可以使用动态规划的方法计算出最短路径

对于任意一条经过所有节点的路径,它的某一个子序列(可以不连续)一定是 0 , 1 , ⋯   , n − 1 0, 1, \cdots, n - 1 0,1,,n1 的一个排列。我们称这个子序列上的节点为「关键节点」。在动态规划的过程中,我们也是通过枚举「关键节点」进行状态转移的。

我们 f [ u ] [ mask ] f[u][\textit{mask}] f[u][mask] 表示从任一节点开始到节点 u u u 为止,并且经过的「关键节点」对应的二进制表示为 m a s k mask mask 时的最短路径长度。由于 u u u 是最后一个「关键节点」,那么在进行状态转移时,我们可以枚举上一个「关键节点」 v v v ,即:
f [ u ] [ mask ] = min ⁡ v ∈ mask , v ≠ u { f [ v ] [ mask \ u ] + d ( v , u ) } f[u][\textit{mask}] = \min_{v \in \textit{mask}, v \neq u} \big\{ f[v][\textit{mask}\backslash u] + d(v, u) \big\} f[u][mask]=vmask,v=umin{f[v][mask\u]+d(v,u)}

其中 mask \ u \textit{mask} \backslash u mask\u 表示将 m a s k mask mask 的第 u u u 位从 1 1 1 变为 0 0 0 后的二进制表示。也就是说,「关键节点」 v v v m a s k mask mask 中的对应位置必须为 1 1 1 ,将 f [ v ] [ mask \ u ] f[v][\textit{mask} \backslash u] f[v][mask\u] 加上从 v v v 走到 u u u 的最短路径长度为 d ( v , u ) d(v,u) d(v,u) ,取最小值即为 f [ u ] [ m a s k ] f[u][mask] f[u][mask]

最终的答案即为: min ⁡ u f [ u ] [ 2 n − 1 ] \min_u f[u][2^n - 1] uminf[u][2n1]
细节:当 m a s k mask mask 中只包含一个 1 1 1 时,我们无法枚举满足要求的上一个「关键节点」 v v v 。这里的处理方式与方法一中的类似:若 m a s k mask mask 中只包含一个 1 1 1 ,说明我们位于开始的节点,还未经过任何路径,因此状态转移方程直接写为:
f [ u ] [ m a s k ] = 0 f[u][mask]=0 f[u][mask]=0
此外,在状态转移方程中,我们需要多次求出 d ( v , u ) d(v, u) d(v,u) ,因此我们可以考虑在动态规划前将所有的 d ( v , u ) d(v,u) d(v,u) 预处理出来。这里有两种可以使用的方法,时间复杂度均为 O ( n 3 ) O(n^3) O(n3)

  • 我们可以使用 F l o y d Floyd Floyd 算法求出所有点对之间的最短路径长度;
  • 我们可以进行 n n n 次广度优先搜索,第 i i i 次从节点 i i i 出发,也可以得到所有点对之间的最短路径长度。
class Solution {
public:
    int shortestPathLength(vector<vector<int>>& g) {
        int n = g.size();
        vector<vector<int>> d(n, vector<int>(n, n + 1));
        for (int i = 0; i < n; ++i) for (int j : g[i]) 
            d[i][j] = 1;
        // 使用floyd算法预处理出所有点对之间的最短路径长度
        for (int k = 0; k < n; ++k)
            for (int i = 0; i < n; ++i)
                for (int j = 0; j < n; ++j)
                    d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
        vector<vector<int>> f(n, vector<int>(1 << n, INT_MAX / 2));
        for (int mask = 1; mask < (1 << n); ++mask) {
            // 如果mask只包含一个1,即是2的幂
            if ((mask & (mask - 1)) == 0) {
                int u = __builtin_ctz(mask);
                f[u][mask] = 0; // 从某一点开始到u为止,经过的关键节点对应的二进制表示为mask时的最短路径长度
            } else {
                for (int u = 0; u < n; ++u) {
                    if (mask & (1 << u)) { // 如果经过了点u
                        for (int v = 0; v < n; ++v) { // 枚举上一个关键节点
                            if ((mask & (1 << v)) && u != v)
                                f[u][mask] = min(f[u][mask], f[v][mask ^ (1 << u)] 
                                    + d[v][u]);
                        }
                    }
                }
            }
        }
        int ans = INT_MAX;
        for (int u = 0; u < n; ++u) ans = min(ans, f[u][(1 << n) - 1]);
        return ans;
    }
};

复杂度分析:

  • 时间复杂度: O ( n 2 ⋅ 2 n ) O(n^2 \cdot 2^n) O(n22n) 。状态的总数为 O ( n ⋅ 2 n ) O(n \cdot 2^n) O(n2n) ,对于每一个状态,我们需要 O ( n ) O(n) O(n) 的时间枚举 v v v 进行状态转移,因此总时间复杂度 O ( n 2 ⋅ 2 n ) O(n^2 \cdot 2^n) O(n22n) 。预处理所有 d ( u , v ) d(u, v) d(u,v) 的时间复杂度为 O ( n 3 ) O(n^3) O(n3) ,但其在渐近意义下小于 O ( n 2 ⋅ 2 n ) O(n^2 \cdot 2^n) O(n22n) ,因此可以忽略。
  • 空间复杂度: O ( n ⋅ 2 n ) O(n \cdot 2^n) O(n2n) ,即为存储所有状态需要使用的空间。

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

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

相关文章

深度解析shell脚本的命令的原理之pwd

pwd是Print Working Directory的缩写&#xff0c;是一个Unix和Linux shell命令&#xff0c;用于打印当前工作目录的绝对路径。以下是对这个命令的深度解析&#xff1a; 获取当前工作目录&#xff1a;pwd命令通过调用操作系统提供的getcwd&#xff08;或相应的&#xff09;系统调…

带自动采集小说网站源码 小说听书网站源码 小说网站源码 带教程

PTCMS可听书可下载的小说站源码 带自动采集和搭建视频教程 必装环境&#xff1a;Nginx(apache.iis也可)&#xff0c;mysql,php5.6,memcached php5.6安装扩展memcache新建站点&#xff0c;注意新建时&#xff0c;PHP版本必须选择PHP5.6 安装教程 1.上传网站文件到网站目录&…

看完这篇 教你玩转渗透测试靶机Vulnhub——Grotesque:1.0.1

Vulnhub靶机Grotesque&#xff1a;1.0.1渗透测试详解 Vulnhub靶机介绍&#xff1a;Vulnhub靶机下载&#xff1a;Vulnhub靶机安装&#xff1a;①&#xff1a;信息收集&#xff1a;②&#xff1a;漏洞利用GetShell&#xff1a;③&#xff1a;Keepass文件的解密&#xff1a;④&…

科技抗老新突破,香港美容仪品牌内地重磅上市

近年来&#xff0c;新消费时代“颜值经济”的火热促使美容行业市场规模增长迅速&#xff0c;越来越多的人愿意为“美”买单&#xff0c;对美的需求也随之增长&#xff0c;美容行业已经成为成长最快的新锐产业。随着经济和科技的发展&#xff0c;“快捷”也成为了当今社会的时代…

【网络】计算机网络基础

Linux网络 对网络的理解 在网络传输中存在的问题&#xff1a; 找到我们所需要传输的主机解决远距离数据传输丢失的问题怎么进行数据转发&#xff0c;路径选择的问题 有问题&#xff0c;就有解决方案&#xff1b; 我们把相同性质的问题放在一起&#xff0c;做出解决方案 解…

【C++】深拷贝和浅拷贝 ③ ( 浅拷贝内存分析 )

文章目录 一、浅拷贝内存分析1、要分析的代码2、调用有参构造函数创建 Student 实例对象3、调用默认拷贝构造函数为新对象赋值4、修改拷贝对象成员变量指针指向的数据5、析构报错 一、浅拷贝内存分析 1、要分析的代码 下面的代码中 , 没有定义拷贝构造函数 , 因此 C 编译器会自…

无涯教程-JavaScript - CSC函数

描述 CSC函数返回以弧度指定的Angular的余割值。 语法 CSC (number)争论 Argument描述Required/OptionalNumberThe angle (in radians) that you want to calculate the cosecant of.Required Notes CSC(n)等于1/SIN(n) 如果Angular为度,则将其乘以PI()/180或使用RADIANS…

每日一题 198. 打家劫舍

难度&#xff1a;中等 这是昨天的每日一题忘记做了&#xff0c;没想到正好是今天的题目的简化版&#xff0c;详细思路可以看http://t.csdn.cn/Smr0t 代码&#xff1a; class Solution:def rob(self, nums: List[int]) -> int:if len(nums) 1:return nums[0]a, b nums[0…

怒刷LeetCode的第3天(Java版)

目录 第一题 题目来源 题目内容 解决方法 方法一&#xff1a;动态规划 第二题 题目来源 题目内容 解决方法 方法一&#xff1a;模拟 方法二&#xff1a;数学规律 方法三&#xff1a;分组 第三题 题目来源 题目内容 解决方法 方法一&#xff1a;数学方法 方法…

001-项目介绍

项目介绍 文章目录 项目介绍编写目的小小说明 项目介绍目前状态目录项目介绍第一代第二代第三代软件部署硬件篇 总结 关键字&#xff1a; Qt、 Qml、 分享、 记录、 目录 编写目的 这是我目前参与的一个真实项目&#xff0c;而且我非常幸运地能够从头到尾地参与其中。在面…

微信小程序|自定义弹窗组件

目录 引言小程序的流行和重要性自定义弹出组件作为提升用户体验和界面交互的有效方式什么是自定义弹出组件自定义弹出组件的概念弹出层组件在小程序中的作用和优势为什么需要自定义弹出组件现有的标准弹窗组件的局限性自定义弹出组件在解决这些问题上的优势最佳实践和注意事项避…

Shell脚本编写:从零到精通

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

C#-WinForm-发送邮件

登录QQ邮箱——设置——开启“POP3/SMTP服务” 登陆QQ邮箱→打开设置→开启“POP3/SMTP服务”&#xff0c;获取“授权码” 简单总结一下&#xff1a; 1、使用SmtpClient发送电子邮件是很简单的&#xff0c;只要正确创建了MailMessage对象和SmtpClient就可以很容易的发送出去电…

Mybatis-Plus入门(1)

单表的CRUD功能代码重复度很高&#xff0c;也没有什么难度。而这部分代码量往往比较大&#xff0c;开发起来比较费时。因此&#xff0c;目前企业中都会使用一些组件来简化或省略单表的CRUD开发工作。目前在国内使用较多的一个组件就是MybatisPlus. 官方网站如下&#xff1a; 简…

超级好用绘图工具(Draw.io+Github)

超级好用绘图工具&#xff08;Draw.ioGithub&#xff09; 方案简介 绘图工具&#xff1a;Draw.io 存储方式&#xff1a; Github 1 Draw.io 1.2 简介 ​ 是一款免费开源的在线流程图绘制软件&#xff0c;可以用于创建流程图、组织结构图、网络图、UML图等各种类型的图表。…

【面试题】forEach能跳出循环吗?

前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 【国庆头像】- 国庆爱国 程序员头像&#xff01;总有一款适合你&#xff01; 如果面试官&#xff0c;或者有人问你foreach怎么跳出循环&#xff0c;请你…

LeetCode 2596. 检查骑士巡视方案

【LetMeFly】2596.检查骑士巡视方案 力扣题目链接&#xff1a;https://leetcode.cn/problems/check-knight-tour-configuration/ 骑士在一张 n x n 的棋盘上巡视。在有效的巡视方案中&#xff0c;骑士会从棋盘的 左上角 出发&#xff0c;并且访问棋盘上的每个格子 恰好一次 。…

关于感恩教师的演讲稿格式及范例

关于感恩教师的演讲稿格式及范例 感恩教师的演讲稿格式应该是&#xff1a;开头感谢听众&#xff0c;正文部分根据题目内容书写&#xff0c;结尾部分再次感谢听众。 以下是一篇关于感恩教师的演讲稿的例子&#xff1a; 尊敬的老师&#xff0c;亲爱的同学们&#xff1a; 大家好…

循环结构在反汇编中特征

本文将使用IDA分析C语言中循环结构(do while&#xff0c;while&#xff0c;for)在反汇编中的特征 目录 IDA分析 do whhile 循环 IDA分析 while 循环 IDA分析 for 循环 do while while和for哪个效率高 IDA分析 do whhile 循环 测试代码 #include <stdio.h> int main…

基于SSM框架的《超市订单管理系统》Web项目开发(第二天)完成登录模块和用户退出模块

《超市订单管理系统》&#xff08;第二天&#xff09; 基于SSM框架的Web项目开发 ​ 昨天我们实现了登录功能&#xff0c;但是用的是模拟数据。今天我们要链接数据库整合SpirngMybatis&#xff0c;读取数据库中的真实数据&#xff0c;用来跟我们输入的userCode和userPassword进…