LeetCode 332. Reconstruct Itinerary【欧拉回路,通路,DFS】困难

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

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

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

给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

  • 例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前。

假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次只能用一次

示例 1:

输入:tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
输出:["JFK","MUC","LHR","SFO","SJC"]

示例 2:

输入:tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
输出:["JFK","ATL","JFK","SFO","ATL","SFO"]
解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"] ,但是它字典排序更大更靠后。

提示:

  • 1 <= tickets.length <= 300
  • tickets[i].length == 2
  • fromi.length == 3
  • toi.length == 3
  • fromi 和 toi 由大写英文字母组成
  • fromi != toi

本题和「753. 破解保险箱」类似,是力扣平台上为数不多的求解欧拉回路 / 欧拉通路的题目。

我们化简本题题意:给定一个 O ( n ) O(n) O(n) 个点 O ( m ) O(m) O(m) 条边的图,要求从指定的顶点出发,经过所有的边恰好一次(可以理解为给定起点的「一笔画」问题),使得路径的字典序最小。这种「一笔画」问题与欧拉图或者半欧拉图有着紧密的联系,下面给出定义:

  • 通过图中所有边恰好一次且行遍所有顶点的通路称为欧拉通路;
  • 通过图中所有边恰好一次且行遍所有顶点的回路称为欧拉回路;
  • 具有欧拉回路的无向图称为欧拉图;
  • 具有欧拉通路但不具有欧拉回路的无向图称为半欧拉图

因为本题保证至少存在一种合理的路径,也就告诉了我们,这张图是一个欧拉图或者半欧拉图。我们只需要输出这条欧拉通路的路径即可。如果没有保证至少存在一种合理的路径,我们需要判别这张图是否是欧拉图或者半欧拉图,具体地:

  • 对于无向图 G G G G G G 是欧拉图当且仅当 G G G 是连通的且没有奇度顶点
  • 对于无向图 G G G G G G 是半欧拉图当且仅当 G G G 是连通的且 G G G 中恰有 0 0 0 个或 2 2 2 个奇度顶点
  • 对于有向图 G G G G G G 是欧拉图当且仅当 G G G 的所有顶点属于同一个强连通分量每个顶点的入度和出度相同
  • 对于有向图 G G G G G G 是半欧拉图当且仅当:
    • 如果将 G G G 中的所有有向边退化为无向边时,那么 G G G 的所有顶点属于同一个强连通分量;
    • 最多只有一个顶点的出度与入度差为 1 1 1
    • 最多只有一个顶点的入度与出度差为 1 1 1
    • 所有其他顶点的入度和出度相同

让我们考虑下面的这张图:
500
我们从起点 J F K JFK JFK 出发,合法路径有两条:

  • J F K → A A A → J F K → B B B → J F K JFK→AAA→JFK→BBB→JFK JFKAAAJFKBBBJFK
  • J F K → B B B → J F K → A A A → J F K JFK→BBB→JFK→AAA→JFK JFKBBBJFKAAAJFK

既然要求字典序最小,那么我们每次应该贪心地选择当前节点所连的节点中字典序最小的那一个,并将其入栈。最后栈中就保存了我们遍历的顺序。

为了保证我们能够快速找到当前节点所连的节点中字典序最小的那一个,我们可以使用优先队列存储当前节点所连到的点,每次我们 O ( 1 ) O(1) O(1) 地找到最小字典序的节点,并 O ( log ⁡ m ) O(\log m) O(logm) 地删除它。

然后我们考虑一种特殊情况:
500
当我们先访问 A A A AAA AAA 时,我们无法回到 J F K JFK JFK,这样我们就无法访问剩余的边了。也就是说,当我们贪心地选择字典序最小的节点前进时,我们可能先走入「死胡同」,从而导致无法遍历到其他还未访问的边。于是我们希望能够遍历完当前节点所连接的其他节点后再进入「死胡同」

注意对于每一个节点,它只有最多一个「死胡同」分支。依据前言中对于半欧拉图的描述,只有那个入度与出度差为 1 1 1 的节点会导致死胡同


解法 H i e r h o l z e r Hierholzer Hierholzer 算法

H i e r h o l z e r Hierholzer Hierholzer 算法用于在连通图中寻找欧拉路径,其流程如下:

  1. 从起点出发,进行深度优先搜索。
  2. 每次沿着某条边从某个顶点移动到另外一个顶点时,都需要删除这条边
  3. 如果没有可移动的路径,则将所在节点加入到栈中,并返回

当我们顺序地考虑该问题时,我们也许很难解决该问题,因为我们无法判断当前节点的哪一个分支是「死胡同」分支。不妨倒过来思考。我们注意到只有那个入度与出度差为 1 1 1 的节点会导致死胡同。而该节点必然是最后一个遍历到的节点——对于当前节点而言,走入它的每一个非「死胡同」分支进行深度优先搜索,都将会搜回到当前节点。而从它的「死胡同」分支出发进行深度优先搜索将不会搜回到当前节点。我们可以改变入栈的规则,当遍历完一个节点所连的所有节点后,才将该节点入栈(即逆序入栈),也就是说当前节点的死胡同分支始终优先于其他非「死胡同」分支入栈

这样就能保证我们可以「一笔画」地走完所有边,且最终的栈中逆序地保存了「一笔画」的结果。我们只要将栈中的内容反转,即可得到答案。

class Solution {
public:unordered_map<string, priority_queue<string, vector<string>, std::greater<string>>> vec;vector<string> stk;void dfs(const string& curr) {while (vec.count(curr) && vec[curr].size() > 0) {string tmp = vec[curr].top();vec[curr].pop();dfs(move(tmp));}stk.emplace_back(curr);}vector<string> findItinerary(vector<vector<string>>& tickets) {for (auto& it : tickets) {vec[it[0]].emplace(it[1]);}dfs("JFK"); // JFK要么是欧拉回路的一点,要么是欧拉通路的起点 reverse(stk.begin(), stk.end());return stk;}
};

复杂度分析:

  • 时间复杂度: O ( m log ⁡ m ) O(m \log m) O(mlogm) ,其中 O ( m ) O(m) O(m) 是边的数量。对于每一条边我们需要 O ( log ⁡ m ) O(\log m) O(logm) 地删除它,最终的答案序列长度为 m + 1 m+1 m+1 ,而与 O ( n ) O(n) O(n) 无关。
  • 空间复杂度: O ( m ) O(m) O(m) ,其中 O ( m ) O(m) O(m) 是边的数量。我们需要存储每一条边。

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

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

相关文章

Vue3+ElementUI使用

<!DOCTYPE html> <html> <head><meta charset"UTF-8"><meta name"viewport" content"initial-scale1.0,maximum-scale1.0,minimum-scale1.0,user-scalable0, widthdevice-width"/><!-- 引入样式 --><lin…

【C++】list的模拟实现【完整理解版】

目录 一、list的概念引入 1、vector与list的对比 2、关于struct和class的使用 3、list的迭代器失效问题 二、list的模拟实现 1、list三个基本函数类 2、list的结点类的实现 3、list的迭代器类的实现 3.1 基本框架 3.2构造函数 3.3 operator* 3.4 operator-> 3…

JavaScript 基础 - 第1天笔记

JavaScript 基础 - 第1天 了解变量、数据类型、运算符等基础概念&#xff0c;能够实现数据类型的转换&#xff0c;结合四则运算体会如何编程。 体会现实世界中的事物与计算机的关系理解什么是数据并知道数据的分类理解变量存储数据的“容器”掌握常见运算符的使用&#xff0c;了…

bug总结问题集和知识点集(一)

目录 一 bug问题集1. 端口被占用 二 oracle1. oracle查看版本怎么操作2. oracle数据库&#xff1a;参数个数无效![在这里插入图片描述](https://img-blog.csdnimg.cn/6a2eebc164f9406c81525371893bbd11.png)3. ORACLE数据库如何完整卸载? 三 mybatis1. mybatis用注解如何实现模…

Java手写最大子数组和算法(如Kadane算法)和最大子数组和算法(如Kadane算法)应用拓展案例

Java手写最大子数组和算法&#xff08;如Kadane算法&#xff09;和最大子数组和算法&#xff08;如Kadane算法&#xff09;应用拓展案例 1. 算法思维导图 以下是使用mermaid代码表示的Kadane算法的实现原理&#xff1a; #mermaid-svg-rI7hVAVsP1qtjZK7 {font-family:"tr…

学习Node js:raw-body模块源码解析

raw-body是什么 raw-body的主要功能是处理HTTP请求体的原始数据。它提供了以下核心功能&#xff1a; 解析请求体&#xff1a;可以从HTTP请求中提取原始数据&#xff0c;包括文本和二进制数据。配置选项&#xff1a;通过配置项&#xff0c;可以设置请求体的大小限制、编码方式…

【LeetCode-简单题KMP】232. 用栈实现队列

文章目录 题目方法一&#xff1a;用输入栈和输出栈模拟队列 题目 方法一&#xff1a;用输入栈和输出栈模拟队列 只有输出栈为空的时候才能将输入栈的元素补充到输出栈&#xff0c;否则输出栈不为空&#xff0c;如果再从输入栈往输出栈填充元素&#xff0c;就会弄乱队列的先进先…

【SpringMVC】拦截器JSR303的使用

【SpringMVC】拦截器&JSR303的使用 1.1 什么是JSR3031.2 为什么使用JSR3031.3 常用注解1.4 Validated与Valid区别1.5 JSR快速入门1.5.2 配置校验规则# 1.5.3 入门案例二、拦截器2.1 什么是拦截器2.2 拦截器与过滤器2.3 应用场景2.4 拦截器快速入门2.5.拦截器链2.6登录案列权…

合同矩阵充要条件

两个实对称矩阵合同的充要条件是它们的正负惯性指数相同。 正惯性指数是矩阵正特征值个数&#xff0c;负惯性指数是矩阵负特征值个数。 即合同矩阵的充分必要条件是特征值的正负号个数相同。 证明&#xff1a; 本论证中的所有矩阵先假设为对称矩阵&#xff0c;但不限于对称…

接口测试——接口协议抓包分析与mock_L1

目录&#xff1a; 接口测试价值与体系常见的接口协议接口测试用例设计postman基础使用postman实战练习 1.接口测试价值与体系 接口测试概念 接口&#xff1a;不同的系统之间相互连接的部分&#xff0c;是一个传递数据的通道接口测试&#xff1a;检查数据的交换、传递和控制…

设计模式之职责链模式

职责链模式:使多个对象都有机会处理请求&#xff0c;从而避免请求的发送者和接收者之间的耦合关系。将这个对象连成一条链&#xff0c;并沿着这条链传递该请求&#xff0c;直到有一个对象处理它为止。 这里发出这个请求的客户端并不知道这当中的哪一个对象最终处理这个请求&am…

ajax 中 success 方法的 return

做前后台交互时会经常用到 ajax&#xff0c;有时候会遇到这样的情况&#xff0c;我们在 a 方法中调用 b 方法&#xff0c;b 方法里是一个 ajax&#xff0c;成功请求后会返回一个结果 data&#xff0c;而我们希望通过 b 方法的返回值获取到 data&#xff0c;我们的代码可能是这样…

TCP详解之三次握手和四次挥手

TCP详解之三次握手和四次挥手 1. TCP基本认识 1.1 什么是 TCP TCP是面向连接的、可靠的、基于字节流的传输层通信协议。 1.2 TCP协议段格式 我们先来看看TCP首部协议的格式 我们先来介绍一些与本文关联比较大的字段&#xff0c;其他字段不做详细阐述。 序列号&#xff1a…

2023面试知识点一

1、新生代和老年代的比例 默认的&#xff0c;新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )&#xff0c;即&#xff1a;新生代 ( Young ) 1/3 的堆空间大小。老年代 ( Old ) 2/3 的堆空间大小。其中&#xff0c;新生代 ( …

213. 打家劫舍 II

文章目录 Tag题目来源题目解读解题思路方法一&#xff1a;动态规划 写在最后 Tag 【动态规划】【数组】 题目来源 213. 打家劫舍 II 题目解读 你是一个专业的小偷&#xff0c;现在要偷一排屋子&#xff0c;但是你不能偷相邻的两间屋子&#xff08;这一排房子的首尾是相连的&…

什么是性能调优?方法有哪些?流程是怎样的?

一、性能调优的含义 性能调优通俗来讲就是对计算机硬件、操作系统和应用有相当深入的了解&#xff0c;调节三者之间的关系&#xff0c;实现整个系统&#xff08;包括硬件、操作系统、应用&#xff09;的性能最大化&#xff0c;并能不断的满足现有的业务需求。 在判定软件存在…

将本地构建的镜像推送到远程镜像库,构建多种系统架构支持的Docker镜像并推送到Docker Hub

目录 推送到 Docker Hub前提&#xff1a;需要在 [Docker Hub](https://hub.docker.com/) 创建账户、创建仓库。1. 创建 Dockerfile 和构建镜像&#xff1a;docker build -t2. 登录到远程镜像库&#xff1a;docker login3. 将镜像标记为远程仓库地址&#xff1a;docker tag4. 推…

Ubuntu-server 22.04LTS源码编译apache服务器

1 系统环境 # cat /etc/os-release PRETTY_NAME"Ubuntu 22.04.3 LTS" NAME"Ubuntu" VERSION_ID"22.04" VERSION"22.04.3 LTS (Jammy Jellyfish)" VERSION_CODENAMEjammy IDubuntu ID_LIKEdebian HOME_URL"https://www.ubuntu.co…

Hadoop-Hbase

1. Hbase安装 1.1 安装zookeeper、 hbase 解压至/opt/soft&#xff0c;并分别改名 配置环境变量并source生效 #ZK export ZOOKEEPER_HOME/opt/soft/zk345 export PATH$ZOOKEEPER_HOME/bin:$PATH #HBASE_HOME export HBASE_HOME/opt/soft/hbase235 export PATH$HBASE_HOME/b…

浅显易懂理解傅里叶变换

说起电子硬件专业&#xff0c;那不得不提的就是傅里叶变换了。 大学课程中应该吓倒了很多人&#xff0c;谈傅里叶色变了。 本次就来重新认识一下电子硬件中的傅里叶变化。 首先理解之前&#xff0c;当然是需要先知道傅里叶这位大牛的人物百科啦。 傅里叶是法国数学家&#xff0…