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

本文属于「征服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位置1int 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)) { // 如果经过了点ufor (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.mzph.cn/news/84877.shtml

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

相关文章

【Android知识笔记】进程通信(三)

在上一篇探索Binder通信原理时,提到了内存映射的概念,其核心是通过mmap函数,将一块 Linux 内核缓存区映射到一块物理内存(匿名文件),这块物理内存其实是作为Binder开辟的数据接收缓存区。这里有两个概念,需要理解清楚,那就是操作系统中的虚拟内存和物理内存,理解了这两…

零基础学前端(五)HTML+CSS实战:模仿百度网站首页

1. 该篇适用于从零基础学习前端的小白 2. 初学者不懂代码得含义也要坚持模仿逐行敲代码&#xff0c;以身体感悟带动头脑去理解新知识 一、实战&#xff1a;将百度网站首页补全 上一篇零基础学前端&#xff08;三&#xff09;重点讲解 HTML-CSDN博客我们已经将顶部两侧内容已经…

无聊的一篇博客(如何通过路由器登陆页对固定机器进行网速干扰,如何帮熊孩子戒网瘾)

1. 路由器登陆页面&#xff0c;按钮解析&#xff0c;获取按钮。 2. JavaScript与上传的脚本。 // 获取要点击的按钮A和按钮B元素var isRunning true; // 初始状态为false// 定义一个函数来模拟点击按钮A和按钮B function clickButtons() {if (isRunning) {// 随机生成一个延时…

03.Qt信号槽使用及其原理

一、QT信号槽概念原理讲解 信号槽 类似windows的消息机制信号函数&#xff0c;只发送不需要知道接收者槽函数&#xff08;普通函数&#xff09;&#xff0c;只接收不管通信QObject来绑定 Qt信号槽原理 绑定信号函数和槽函数调用信号函数&#xff08;将信号写入队列&#xf…

理解Nginx反向代理详解

什么是Nginx反向代理&#xff1f; Nginx是一个高性能的开源Web服务器&#xff0c;但它也可以用作反向代理服务器。反向代理是一种服务器配置&#xff0c;它允许Nginx接收客户端请求&#xff0c;然后将这些请求转发到后端服务器&#xff0c;最终将响应返回给客户端。这种配置对于…

CSS 模糊效果 CSS 黑白效果 CSS调整亮度 对比度 饱和度 模糊效果 黑白效果反转颜色

CSS 模糊效果 CSS 黑白效果 CSS调整亮度 饱和度 模糊效果 黑白效果 实现 调整亮度 饱和度 模糊效果 黑白效果 使用 filter1、模糊2、亮度3、对比度4、饱和度5、黑白效果6、反转颜色7、组合使用8、 filer 完整参数 实现 调整亮度 饱和度 模糊效果 黑白效果 使用 filter 1、模糊…

C语言每日一题(7):获得月份天数

文章主题&#xff1a;获得月份天数&#x1f525;所属专栏&#xff1a;C语言每日一题&#x1f4d7;作者简介&#xff1a;每天不定时更新C语言的小白一枚&#xff0c;记录分享自己每天的所思所想&#x1f604;&#x1f3b6;个人主页&#xff1a;[₽]的个人主页&#x1f3c4;&…

学会这篇文章分享的知识,你就超过了90%的测试人

♥ 前 言 jmeter 可以做性能测试&#xff0c;这个很多人都知道&#xff0c;那你知道&#xff0c;jmeter 可以在启动运行时&#xff0c;指定线程数和运行时间&#xff0c;自定义性能场景吗&#xff1f; jmeter 性能测试&#xff0c;动态设定性能场景 平时&#xff0c;我们使…

【验证码逆向专栏】螺丝帽人机验证逆向分析

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;不提供完整代码&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 本文章未…

工作应当有挑战

有挑战 才能有所成长 正所谓人到山前必有路 是挑战 一般就会有未知 未知往往伴随着困难 有困难 并不可怕&#xff0c;也不必自我抱怨&#xff0c;自我抱怨只会陷入无尽的精神内耗 我们只要做好自己 困难就会迎刃而解 如果自己的获得 没有达到自己的期望 其实那也不必气馁 再…

【ICCV 2023】FocalFormer3D : Focusing on Hard Instance for 3D Object Detection

原文链接&#xff1a;https://arxiv.org/abs/2308.04556 1. 引言 目前的3D目标检测方法没有显式地去考虑漏检问题。   本文提出了困难实例探测&#xff08;HIP&#xff09;。受目标检测的级联解码头启发&#xff0c;HIP逐步探测误检样本&#xff0c;极大提高召回率。在每个阶…

UDP 的报文结构

1.UDP特点2.UDP协议报文结构 1.UDP特点 UDP特点分为&#xff1a; 无连接&#xff1a;知道对端的IP和端口号就可以进行传输&#xff0c;即通信时不需要创建连接&#xff08;发送数据结束时也没有连接可以释放&#xff09;所以减小了开销和发送数据前的时延&#xff1b;比如&am…

nginx部署多个项目

前言 实现在一台服务器上使用nginx部署多个项目的方法 查看并修改nginx安装的默认配置文件 在 Linux 操作系统中&#xff0c;Nginx 在编译安装时默认的配置文件路径是 /usr/local/nginx/conf/nginx.conf。 如果是通过发行版的包管理器安装&#xff0c;则默认的配置文件路径可能…

基于数据驱动的成本洞察,趣丸科技的FinOps进阶之路~

今年以来&#xff0c;我们注意到越来越多的单位开始积极实践FinOps&#xff0c;而随着FinOps的发展&#xff0c;大家对于其落地过程的关注也更加具体和深入&#xff0c;涉及了账单波动、FinOps的边际效应、成本模型、依赖工具等多个关键问题。 本月「UGeek大咖说」线上直播活动…

【c语言】贪吃蛇

当我们不想学习新知识的时候&#xff0c;并且特别无聊&#xff0c;就会突然先看看别人怎么写游戏的&#xff0c;今天给大家分享的是贪吃蛇&#xff0c;所需要的知识有结构体&#xff0c;枚举&#xff0c;以及easy-x图形库的一些基本函数就完全够用了&#xff0c;本来我想插入游…

创建一个简单的外卖订餐系统

在今天的快节奏生活中&#xff0c;外卖订餐系统已经成为了人们日常生活中不可或缺的一部分。这些系统通过在线点餐和配送服务&#xff0c;为用户提供了便捷的用餐体验。在本文中&#xff0c;我们将创建一个简单的外卖订餐系统&#xff0c;使用Python和Flask框架构建后端&#x…

Mac电脑系统怎么样才能干干净净地卸载应用程序?

Mac系统怎么样才能干干净净地卸载应用程序&#xff0c;不留下隐私数据和用户信息呢&#xff1f;如果有方法的话&#xff0c;那么该方法对于Mac电脑小白是否友好呢&#xff1f; CleanMyMac就是一款用于清理Mac系统下应用程序的一款清理工具&#xff0c;其内置了应用程序的安全卸…

轻松搞定Spring集成缓存,让你的应用程序飞起来!

Spring集成缓存 缓存接口开启注解缓存注解使用CacheableCachePutCacheEvictCachingCacheConfig 缓存存储使用 ConcurrentHashMap 作为缓存使用 Ehcache 作为缓存使用 Caffeine 作为缓存 主页传送门&#xff1a;&#x1f4c0; 传送 Spring 提供了对缓存的支持&#xff0c;允许你…

Spring MVC常见面试题

Spring MVC简介 Spring MVC框架是以请求为驱动&#xff0c;围绕Servlet设计&#xff0c;将请求发给控制器&#xff0c;然后通过模型对象&#xff0c;分派器来展示请求结果视图。简单来说&#xff0c;Spring MVC整合了前端请求的处理及响应。 Servlet 是运行在 Web 服务器或应用…

内存管理之虚拟内存

本篇遵循内存管理->地址空间->虚拟内存的顺序描述了内存管理、地址空间与虚拟内存见的递进关系&#xff0c;较为详细的介绍了作为在校大学生对于虚拟内存的理解。 内存管理 引入 RAM&#xff08;内存&#xff09;是计算机中非常重要的资源&#xff0c;由于造价的昂贵&…