学习周报W03
前言
本周学习的内容很纯粹:
- 图论 相关
- 动态规划 基础
- C++ Stllib
因为只是学完了图论,但是DP还没学完,所以这周就先总结图论吧,DP留到下周在讲。
图论
现在对于图论有了一个基本认识,知道了如何建图,以及特殊建图方式:“链式前向星”。
学习了拓扑序,最小生成树的概念(以及两种求最小生成树的算法:KruskarAlgo 和 primAlgo,
以及基本的图遍历BFS和双向BFS,单源最短路算法 DijkstraAlgo,并了解了分层图最短路,
最后学习了四种图相关基本算法:A*、Floyd、Bellman-Floyd、SPFA。
整理一下就是:
- 图的建立、链式前向星 -> 2025-03-31
- 拓扑序 -> 2025-03-31
- 最小生成树 -> 2025-04-01
- BFS -> 2025-04-01
- 双向BFS -> 2025-04-02
- Dijkstra算法、分层路最短图 -> 2025-04-02
- A*、Floyd、Bellman-Floyd、SPFA -> 2025-04-02
对于图而言,模板很重要,啃下模板是基础的基础。
在此之上,会有对题目的额外理解,每个题目独有的设计和对应推理过程,
不过那应该是较困难的内容了,对于入门者而言,先把模板背熟才是上策。
然后就是挑选合适模板,总之,对于笔试or比赛而言,A了就是胜利。
时空复杂度低的模板往往没这么考验码力,总之量力而为,一切以A题为准。
至于对题目设计的独有推理,则应该需要长期的刷题刷出“手感”
所以加油吧,认清自己的“入门者”身份,然后不懈努力。
碎碎念:
链式前向星
第一次接触觉得很高大上啊,这个名字。
深入了解之后只佩服名字太贴切了!
“链式”暗示了“算法对边的组织”,“前向”暗示了 “边是出边”,
“星”则很详细的展示了每一个单元的拓扑形象 - 像一个放射边的星。
拓扑序
第一次接触这个概念还是在软件工程的 里程碑问题 中接触到的,
拓扑序揭示了 有向无环图 中的某些依赖关系特征。
双向BFS
双向BFS更像是一个技巧,
无论是用于折半样本遍历样本解空间,还是头尾相向搜索,
实际上都是对整个解空间的大剪枝。
图、图的建立与链式前向星
图
一般来说,图的种类有:
- 有向 vs 无向
- 不带权 vs 带权
在实际题目中,
入参一般为: 节点数量n和所有的边 或者 直接给图。
图的建立
建图的三种方式,我们图解一下!
- 邻接矩阵(适合点的数量不多的图)
- 邻接表(最常用的方式)
- 链式前向星(空间要求严苛情况下使用。比赛必用,大厂笔试、面试不常用)
什么是链式前向星?
链式前向星 是一种用静态数组 模拟 链表跳转的结构,
可以比其他两种方式更节省内存。
其精髓在于对 边 编号,每个节点只保留所连接的 头边 信息,
再通过维护 next数组 构建跳转信息,
其中还要维护 to数组 保存每条边的去向节点信息。
这样就可以像 邻接表 一样查询了。
拓扑序
什么是拓扑序/拓扑排序?
要求:有向图、没有环;
即只有在有向无环图中才存在拓扑序。
拓扑排序的顺序可能不只一种。拓扑排序也可以用来判断有没有环。
1)在图中找到所有入度为0的点
2)把所有入度为0的点在图中删掉,重点是删掉影响!继续找到入度为0的点并删掉影响
3)直到所有点都被删掉,依次删除的顺序就是正确的拓扑排序结果
4)如果无法把所有的点都删掉,说明有向图里有环
重要技巧:
利用拓扑排序的过程,上游节点逐渐推送消息给下游节点的技巧,
这个技巧已经是树型DP的内容了,可以稍微领略一下树形DP。
例题分析
题目1
拓扑排序模版
邻接表建图(动态方式)
测试链接 : https://leetcode.cn/problems/course-schedule-ii
链式前向星建图(静态方式)
测试链接 : https://www.nowcoder.com/practice/88f7e156ca7d43a1a535f619cd3f495c
题目2
字典序最小的拓扑排序
测试链接 : https://www.luogu.com.cn/problem/U107394
1 | 要求返回所有正确的拓扑排序中 字典序最小 的结果 |
注意:解决该问题需要使用小根堆,建图请使用链式前向星方式,因为比赛平台用其他建图方式会卡空间
题目3
火星词典
测试链接 : https://leetcode.cn/problems/alien-dictionary
1 | 现有一种使用英语字母的火星语言 |
实际上就是构建字母的拓扑序。
关键在于找到第一个不同的字母,然后获取字典序大小(依赖)关系。
其中需要注意判断一下是否矛盾,如果矛盾要直接返回空串。
题目4
戳印序列
测试链接 : https://leetcode.cn/problems/stamping-the-sequence
1 | 你想最终得到"abcbc",认为初始序列为"?????"。印章是"abc" |
很难想的题目啊!实际上解题关键在于:后盖的会覆盖前面盖的,
也就是说,遍历的去盖对于 target 上每一个位置上的字母,如果他是正确的,那么他就应该在那里。
如果他不正确,那么可能将来某一次盖章会覆盖掉这个错误,然后我们就可以因此找到这个依赖关系,而有了依赖关系我们就可以构建有向无环图,拿到拓扑序,也就是题解。
题目5
最大食物链计数
测试链接 : https://www.luogu.com.cn/problem/P4017
1 | a -> b,代表a在食物链中被b捕食 |
标准的通过拓扑序过程推送信息的题。
题目6
喧闹和富有
测试链接 : https://leetcode.cn/problems/loud-and-rich/
1 | 从 0 到 n - 1 编号,其中每个人都有不同数目的钱,以及不同程度的安静值 |
在这里我们要注意,
- 构建从有钱为源头指向没钱的有向无环图。
- 拓扑序过程推送的信息是来自 上游 中更安静的那一个(即假如有多个上游,取最值)。
题目7
并行课程 III
测试链接 : https://leetcode.cn/problems/parallel-courses-iii/
1 | 给你一个整数 n ,表示有 n 节课,课程编号从 1 到 n |
经典软件工程中的里程碑问题,实际上和上一题一致,只不过推送的信息改为了先修时间,每个节点的最大修行时间取决于 上游 的最大值。
题目8
参加会议的最多员工数
测试链接 : https://leetcode.cn/problems/maximum-employees-to-be-invited-to-a-meeting/
1 | 一个公司准备组织一场会议,邀请名单上有 n 位员工 |
好抽象的题。
通过分析,可以得出两种情况
- 可能性1 : 所有小环(中心个数 == 2),算上中心点 + 延伸点,总个数
- 可能性2 : 所有大环(中心个数 > 2),只算中心点,最大环的中心点个数
答案取两种情况的最值。
拓扑序主要在这里起到剥离链找到环的作用,并且通过推送技巧可以把环上每一个节点所链接的最长链的长度记录上。
最小生成树(MST)
最小生成树(Minimum Spanning Tree, MST) 是图论中的一个重要概念,通常用于优化网络连接问题。
定义:在一个连通无向图中,连接所有顶点的最小权重的树。
特点:
1. 包含所有顶点:MST包含图中的所有顶点。
2. 无环(Acyclic):MST是一棵树,不包含任何环。
3. 最小权重(Minimum Weight):MST的边权和最小。
最小生成树可能不只一棵,只要保证边的总权值最小,就都是正确的最小生成树
如果无向带权图有n个点,那么最小生成树一定有n-1条边
扩展:最小生成树一定是最小瓶颈树
最小瓶颈生成树(MBST)
最小瓶颈生成树(MBST)是最小生成树(MST)的一个变种,侧重于最小化树中最大权重的边。
目标:在所有连接整个图的生成树(Spanning Tree)中,使得最大的边权最小。
区别:MST 关注的是所有边权之和最小,而 MBST 关注的是最重边的权值最小。
最小瓶颈生成树不一定是唯一的,但所有MBST的最大边权是相同的。
所有的MST都是MBST(但MBST不一定是MST)。
MST的最大权重边就是MBST的最大权重边(因为MST已经是所有可能树中最优的)。
MST求解 - Kruskal算法(最常用)
- 把所有的边,根据权值从小到大排序,从权值小的边开始考虑
- 如果连接当前的边不会形成环,就选择当前的边
- 如果连接当前的边会形成环,就不要当前的边
- 考察完所有边之后,最小生成树的也就得到了
时间复杂度O(m * log m) + O(n) + O(m)
最简单,不用多少脑子,利用并查集实现。
MST求解 - Prim算法(不算常用)
- 解锁的点的集合叫set(普通集合)、解锁的边的集合叫heap(小根堆)。set和heap都为空。
- 可从任意点开始,开始点加入到set,开始点的所有边加入到heap
- 从heap中弹出权值最小的边e,查看边e所去往的点x:
- 如果x已经在set中,边e舍弃,重复步骤3
- 如果x不在set中,边e属于最小生成树,把x加入set,重复步骤3
- 当heap为空,最小生成树的也就得到了
时间复杂度O(n + m) + O(m * log m)
从代码实现以及算法时空复杂度考虑,未优化的版本基本上都劣于 Kruskal算法。
Prim算法的优化(比较难)
- 小根堆里放(节点,到达节点的花费),根据 到达节点的花费 来组织小根堆
- 小根堆弹出(u节点,到达u节点的花费y),y累加到总权重上去,然后考察u出发的每一条边, 假设,u出发的边,去往v节点,权重w
- 如果v已经弹出过了(发现过),忽略该边
- 如果v从来没有进入过堆,向堆里加入记录(v, w)
- 如果v在堆里,且记录为(v, x)
- 如果w < x,则记录更新成(v, w),然后调整该记录在堆中的位置(维持小根堆)
- 如果w >= x,忽略该边
- 重复 步骤2,直到小根堆为空
时间复杂度O(n+m) + O((m+n) * log n)
优化之后,复杂度取决于节点数量,所以更适用于稠密图。
例题分析:
题目1
返回最小生成树的最小权值和
测试链接 : https://www.luogu.com.cn/problem/P3366
最小生成树(MST) 模板题。
题目2
水资源分配优化
测试链接 : https://leetcode.cn/problems/optimize-water-distribution-in-a-village/
1 | 村里面一共有 n 栋房子。我们希望通过建造水井和铺设管道来为所有房子供水。 |
可以假定有一个水源,然后水源可以通向每一个村庄,这些路的代价就是每个村庄打井的代价,然后求 最小生成树(MST) 即可。
题目3
检查边长度限制的路径是否存在
测试链接 : https://leetcode.cn/problems/checking-existence-of-edge-length-limited-paths/
1 | 给你一个 n 个点组成的无向图边集 edgeList |
实际上就是求一个符合规则的 最小生成树(MST),然后再检测这个树中是否包含这两个节点。
题目4
繁忙的都市
测试链接 : https://www.luogu.com.cn/problem/P2330
1 | 一个非常繁忙的大都市,城市中的道路十分的拥挤,于是市长决定对其中的道路进行改造 |
最小生成树(MST) 一定同时也是 最小瓶颈树(MBST)。
所以,题目就是让你求 最小生成树(MST)。
广度优先搜索(BFS)
广度优先搜索(Breadth-First Search, BFS)
是一种用于遍历或搜索图(Graph)或树(Tree)的算法。它按照层级(Level Order)逐层向外扩展,优先访问当前层的所有节点,然后再访问下一层的节点。
BFS 适用于:
- 最短路径问题(无权图/网格)
- 连通性检查
- 状态搜索问题(如棋类游戏的最优解、最少转换次数等)
bfs的特点是逐层扩散,从源头点到目标点扩散了几层,最短路就是多少
bfs可以使用的特征是 任意两个节点之间的相互距离相同(无向图)
bfs开始时,可以是 单个源头、也可以是 多个源头
bfs频繁使用队列,形式可以是 单点弹出 或者 整层弹出
bfs进行时,进入队列的节点需要标记状态,防止 同一个节点重复进出队列
bfs进行时,可能会包含 剪枝策略 的设计
bfs是一个理解难度很低的算法,难点在于 节点如何找到路、路的展开 和 剪枝设计
01BFS
01bfs,适用于 图中所有边的权重只有0和1两种值,求源点到目标点的最短距离
时间复杂度为 O(节点数量+边的数量)
distance[i]
表示从源点到i点的最短距离,初始时所有点的distance设置为无穷大- 源点进入双端队列,
distance[源点]=0
- 双端队列 头部弹出 x,
- 如果x是目标点,返回
distance[x]
表示源点到目标点的最短距离 - 考察从x出发的每一条边,假设某边去y点,边权为w
- 如果
distance[y] > distance[x] + w
,处理该边;否则忽略该边 - 处理时,更新
distance[y] = distance[x] + w
如果w == 0,y从头部进入双端队列;如果w == 1,y从尾部进入双端队列 - 考察完x出发的所有边之后,重复步骤3
- 如果
- 如果x是目标点,返回
- 双端队列为空停止
为什么不能用 传统BFS ?- 因为点与点的权不是一样的,
可以理解为传统BFS适用图的权都是0。
为什么不需要 Visited数组 标记? - 因为进队的节点要不就是下一层节点,
要不就是同层节点(边权 1 或者 边权 0)就算前面的点对后面的点发生修正,也最多修正一次,而且流程中会忽略掉该修正后的点, distance数组 起到了标记的作用。
补充知识:
- 曼哈顿距离( Manhattan Distance):
- (x0, y0) 和 (x1, y1) 这两个单元格之间的距离是 |x0 - x1| + |y0 - y1|
- 宽度优先遍历及其扩展:
- 宽度优先遍历与优先级队列结合 - Dijkstra算法。
- 可以用宽度优先遍历与深度优先遍历结合,去生成路径:
- bfs建图
- dfs利用图生成路径
例题分析:
题目1
地图分析
测试链接 : https://leetcode.cn/problems/as-far-from-land-as-possible/
1 | 你现在手里有一份大小为 n x n 的 网格 grid |
经典 BFS 问题,直接套用BFS模板返回深度减一即可。
题目2
贴纸拼词
测试链接 : https://leetcode.cn/problems/stickers-to-spell-word/
1 | 我们有 n 种不同的贴纸。每个贴纸上都有一个小写的英文单词。 |
可以用DP做,也可以用BFS暴搜,关键在于剪枝的设计。
本题剪枝的设计在于 - “每一次选择都尽可能的消除前缀”。
前缀是来自于题目 target 的设计,因为排序不影响结果,所以排序之后,
会出现类似 “aaabbbccff……”的字符串。
对于 stickers 而言,也是一样,在排序之后,也会出现类似“aaabbcc……”的字符串。
所以对于每层搜索,即每轮 选择 stickers 的过程中,都尽可能的去选择可以消除前缀的字符串,所以这就要求我们额外维护一个26个字符大小的 List 去记录那些可以消除对应前缀的词,既可以记录,也可以防止再次访问。
题目3
到达角落需要移除障碍物的最小数目
测试链接 : https://leetcode.cn/problems/minimum-obstacle-removal-to-reach-corner/
1 | 给你一个下标从 0 开始的二维整数数组 grid ,数组大小为 m x n |
把墙看作 权 为1的边,通路看作 权 为0的边,题目转化为 01BFS模板题。
题目4
使网格图至少有一条有效路径的最小代价
测试链接 : leetcode.cn/problems/minimum-cost-to-make-at-least-one-valid-path-in-a-grid
1 | 给你一个 m * n 的网格图 grid 。 grid 中每个格子都有一个数字 |
顺着箭头走可以看作 权 为0的边,花费cost走可以看作 权 为 1的边,转换为标准 01BFS模板。
题目5
二维接雨水
测试链接 : https://leetcode.cn/problems/trapping-rain-water-ii/
1 | 给你一个 m * n 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度 |
前置题目:双指针技巧 - 42. 接雨水
技巧:1. 宽度优先遍历与优先级队列结合
其核心在于考察薄弱点。
维护一个 以水线为比较逻辑 的 小根堆。
先将边缘点加入 小根堆 中然后每次对堆顶操作:
将堆顶节点的 影响节点 加入堆中。
如果 可以影响水线,则加入时传播水线影响,如果 不能影响 则保持原高度。
时间复杂度:O(n * m * lbm)
#### 题目6
单词接龙 II
测试链接 : https://leetcode.cn/problems/word-ladder-ii/
1
2
3
4
5
6
7
8
9
10
11
按字典 wordList 完成从单词 beginWord 到单词 endWord 转化
一个表示此过程的 转换序列 是形式上像
beginWord -> s1 -> s2 -> ... -> sk 这样的单词序列,并满足:
每对相邻的单词之间仅有单个字母不同
转换过程中的每个单词 si(1 <= i <= k)必须是字典 wordList 中的单词
注意,beginWord 不必是字典 wordList 中的单词
sk == endWord
给你两个单词 beginWord 和 endWord ,以及一个字典 wordList
请你找出并返回所有从 beginWord 到 endWord 的 最短转换序列
如果不存在这样的转换序列,返回一个空列表
每个序列都应该以单词列表 [beginWord, s1, s2, ..., sk] 的形式返回
要点:
1. bfs反向建图:正向遍历,逆向索引。
2. dfs利用构建的 反向图 生成路径。
3. 反向比对 <- 不是通过遍历表内字符比对,而是主动改变单词自身然后去表中比对(假设是hash表)
1 | 按字典 wordList 完成从单词 beginWord 到单词 endWord 转化 |
双向BFS
前置知识:
- 经典递归过程解析
- 根据数据量猜解法
- 宽度优先遍历及其扩展
双向BFS(Bidirectional Breadth-First Search)
双向BFS(Bidirectional Breadth-First Search)是一种高效的图搜索算法,它通过从起点和终点同时进行广度优先搜索(BFS),直到两个搜索方向在某个节点相遇,从而找到起点到终点的最短路径。相比传统的单向BFS,双向BFS通常在搜索空间较大时具有更高的效率。
双向BFS相较于单向BFS有显著优势:
- 搜索空间减小:由于从起点和终点同时逼近,搜索范围大大缩小,通常只需探索图中较小的一部分。
- 时间复杂度优化:单向BFS的时间复杂度为O(b^d)(b是分支因子,d是路径深度),而双向BFS的最坏时间复杂度为O(b^{d/2}),效率更高。
双向广搜常见用途:
- 小优化 - bfs的剪枝策略,分两侧展开分支,哪侧数量少就从哪侧展开
- 用于解决特征很明显的一类问题:
- 特征:全量样本不允许递归完全展开,但是半量样本可以完全展开
- 过程:把数据分成两部分,每部分 各自展开 计算结果,然后设计两部分结果的 整合逻辑
例题分析:
题目1
单词接龙
测试链接 : https://leetcode.cn/problems/word-ladder
1 | 字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 |
参考 [[2025-04-01]] <- 126. 单词接龙 II
本题使用 双向BFS ,从 beginWord 和 endWord 相向进行BFS。
维护三个 HashSet 用于存储较小的一层,较大的一层和较小的一层拓展出的下一层。
每次BFS都让较小的一层往下拓展,直到相遇或者无可拓展为止。
另外,实际上 dict表 也起到了 Visited数组 的作用。
时间复杂度:O(26 * wordLength * wordListSize)
题目2
零食问题 & 世界冰球锦标赛
测试链接 : https://www.nowcoder.com/practice/d94bb2fa461d42bcb4c0f2b94f5d4281
测试链接 : https://www.luogu.com.cn/problem/P4799
1 | 牛牛准备参加学校组织的春游, 出发前牛牛准备往背包里装入一些零食, 牛牛的背包容量为w |
看似背包问题让人秒想到DP,实际上数据量太大,DP建表直接给你内存干爆。
所以只能暴力搜索,但是2^40的的超大规模不可能搜的完啊,
这时候就要用到技巧:我们可以将输入分为左右两个区间,
分别对每一个部分进行暴力搜索,然后开两个数组分别收集左右区间返回值,
然后排序,不回退双指针收集答案。
时间复杂度:O(2^(N/2) + 2^(N/2) + 2^(N/2))
题目3
最接近目标值的子序列和
测试链接 : https://leetcode.cn/problems/closest-subsequence-sum
1 | 给你一个整数数组 nums 和一个目标值 goal |
同上
Dijkstra算法、分层图最短路
前置知识:
- 堆结构
- 位图,用一个整型变量最多可以表示32个状态,并且非常方便、快速
- 建图、链式前向星
- 最小生成树 - prim算法利用反向索引堆做的优化
- 宽度优先遍历及其扩展
Dijkstra算法及其优化
Dijkstra算法:给定一个源点,求解从源点到每个点的最短路径长度,即求单源最短路径算法。
适用范围:有向图、边的权值没有负数
代码实现:
彻底暴力的实现:时间复杂度太差、无意义
普通堆实现的Dijkstra算法:最普遍、最常用
- 算法核心过程:
- 节点弹出过就忽略
- 节点没弹出过,让其它没弹出节点距离变小的记录加入堆
- 算法核心过程:
反向索引堆实现的Dijkstra算法:最快速、最极致 - 核心在于掌握反向索引堆
斐波那契堆优化Dijkstra算法:理论最优,但是常数时间大,代码难度大。
暴力实现的Dijkstra算法 - 朴素 Dijkstra
时间复杂度:O(N²)
- 初始化:
- 将源节点的距离设为0,其他节点的距离设为无穷大(通常用INF表示)。
- 创建一个集合(通常用布尔数组表示)来标记已处理(即已找到最短路径)的节点,初始时为空。
- 迭代:
- 在未处理的节点中,找到距离源节点当前最小的节点。
- 将该节点标记为已处理。
- 更新该节点的所有邻接节点的距离:如果通过该节点到达邻接节点的路径比已知路径更短,则更新距离。
- 终止:
- 重复上述步骤,直到所有节点都被处理,或者剩余节点不可达。
普通堆实现的Dijkstra算法
时间复杂度:O((N+M) log N)
distance[i]
表示从源点到i点的最短距离,visited[i]
表示i节点是否从小根堆弹出过- 准备好小根堆,小根堆存放记录:(x点,源点到x的距离),小根堆根据距离组织
- 令
distance[源点]=0
,(源点,0)进入小根堆 - 从小根堆弹出(u点,源点到u的距离)
- 如果
visited[u] == true
,不做任何处理,重复步骤4 - 如果
visited[u] == false
,令visited[u] = true
,u就算弹出过了
- 然后考察u的每一条边,假设某边去往v,边权为w
- 如果
visited[v] == false
并且distance[u] + w < distance[v]
令distance[v] = distance[u] + w
,把(v, distance[u] + w)
加入小根堆 - 处理完u的每一条边之后,重复步骤4
- 如果
- 如果
- 小根堆为空过程结束,distance表记录了源点到每个节点的最短距离。
反向索引堆实现的Dijkstra算法
时间复杂度:O(M log N)
- 准备好反向索引堆,根据源点到当前点的距离组织小根堆,可以做到如下操作
- 新增记录(x, 源点到x的距离)
- 当源点到x的距离更新时,可以进行堆的调整
- x点一旦弹出,以后忽略x
- 弹出堆顶的记录(u, 源点到u的距离)
- 把(源点,0)加入反向索引堆,过程开始
- 反向索引堆弹出(u,源点到u的距离),考察u的每一条边,假设某边去往v,边权为w
- 如果v没有进入过反向索引堆里,新增记录(v, 源点到u的距离 + w)
- 如果v曾经从反向索引堆弹出过,忽略
- 如果v在反向索引堆里,看看源点到v的距离能不能变得更小,如果能,调整堆;不能,忽略
- 处理完u的每一条边,重复步骤3
- 引堆为空过程结束。反向索引堆里记录了源点到每个节点的最短距离。
分层图最短路
分层图最短路,又叫扩点最短路。
不把实际位置看做图上的点,而是把 实际位置及其状态的组合 看做是图上的点,然后搜索
bfs 或者 Dijkstra的过程不变,只是扩了点(分层)而已
原理简单,核心在于如何扩点、如何到达、如何算距离,每个题可能都不一样
例题分析:
题目1
Dijkstra算法模版
普通堆的实现
测试链接: https://leetcode.cn/problems/network-delay-time
反向索引堆的实现
测试链接: https://www.luogu.com.cn/problem/P4779
题目2
最小体力消耗路径
测试链接: https://leetcode.cn/problems/path-with-minimum-effort/
1 | 你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights |
经典Dijkstra算法模板题
题目3
水位上升的泳池中游泳
测试链接: https://leetcode.cn/problems/swim-in-rising-water/
1 | 在一个 n x n 的整数矩阵 grid 中 |
经典Dijkstra算法模板,就是要搞清楚什么是代价,在这里,代价就是你要额外等的时间。
题目4
获取所有钥匙的最短路径
测试链接: https://leetcode.cn/problems/shortest-path-to-get-all-keys
1 | 给定一个二维网格 grid ,其中: |
经典分层图最短路 + BFS,就是在考虑路径的同时考虑当前路径的状态,以此扩点分层。
题目5
电动车游城市
测试链接: https://leetcode.cn/problems/DFPeFJ/
1 | 小明的电动车电量充满时可行驶距离为 cnt |
经典分层最短路 + Dijkstra算法,
本题扩点的思路:按照 城市 以及到达 城市 时的 电量 以及 花费时间 扩点。
题目6
飞行路线
测试链接 : https://www.luogu.com.cn/problem/P4568
1 | Alice和Bob现在要乘飞机旅行,他们选择了一家相对便宜的航空公司 |
经典分层最短路 + Dijkstra算法,
本题扩点的思路:按照 免单次数 设置状态以扩点。
A*、Floyd、Bellman-Floyd、SPFA
前置知识:
- 建图、链式前向星
- 宽度优先遍历
- Dijkstra算法
A* 算法
A* 算法:指定源点,指定目标点,求源点到达目标点的最短距离
和Dijkstra算法类似,是单源最短路算法
增加了当前点到终点的预估函数
在堆中根据 从源点出发到达当前点的距离+当前点到终点的预估距离 来进行排序
剩下的所有细节和Dijskra算法完全一致
预估函数要求:==当前点到终点的预估距离 <= 当前点到终点的真实最短距离
预估函数是一种吸引力:
- 合适的吸引力可以提升算法的速度,吸引力过强会出现错误
- 保证 预估距离 <= 真实最短距离 的情况下,尽量接近真实最短距离,可以做到功能正确 且 最快
预估终点距离经常选择:
- 曼哈顿距离
- 欧式距离
- 对角线距离(切比雪夫距离)
Floyd算法
Floyd算法,得到图中任意两点之间的最短距离
时间复杂度O(n^3),空间复杂度O(n^2),常数时间小,容易实现
适用于任何图,不管有向无向、不管边权正负、但是不能有负环(保证最短路存在)
过程简述:
distance[i][j]
表示i和j之间的最短距离distance[i][j] = min ( distance[i][j] , distance[i][k] + distance[k][j])
- 枚举所有的k即可,实现时一定要最先枚举跳板!
Bellman-Ford算法
Bellman-Ford算法,
解决可以有负权边但是不能有负环(保证最短路存在)的图的单源最短路算法
- 松弛操作:
假设源点为A,从A到任意点F的最短距离为distance[F]
假设从点P出发某条边,去往点S,边权为W
如果发现,distance[P] + W < distance[S]
,也就是通过该边可以让distance[S]
变小
那么就说,P出发的这条边对点S进行了松弛操作
Bellman-Ford过程
- 每一轮考察每条边,每条边都尝试进行 松弛操作,那么若干点的distance会变小
- 当某一轮发现不再有 松弛操作 出现时,算法停止
Bellman-Ford算法时间复杂度:
假设点的数量为N,边的数量为M,每一轮时间复杂度O(M)
最短路存在的情况下,因为1次松弛操作会使1个点的最短路的边数+1
而从源点出发到任何点的最短路最多走过全部的n个点,所以松弛的轮数必然 <= n - 1
所以Bellman-Ford算法时间复杂度O(M*N)
重要推广:判断从某个点出发能不能到达负环
上面已经说了,如果从A出发存在最短路(没有负环),
那么松弛的轮数必然 <= n - 1
而如果从A点出发到达一个负环,那么松弛操作显然会无休止地进行下去
所以,如果发现从A点出发,在第n轮时松弛操作依然存在,说明从A点出发能够到达一个负环
Bellman-Ford + SPFA优化(Shortest Path Faster Algorithm)
很轻易就能发现,每一轮考察所有的边看看能否做松弛操作是不必要的
因为只有上一次被某条边松弛过的节点,所连接的边,才有可能引起下一次的松弛操作
所以用队列来维护 “这一轮哪些节点的distance变小了”
下一轮只需要对这些点的所有边,考察有没有松弛操作即可
- SPFA只优化了常数时间,在大多数情况下跑得很快,但时间复杂度为O(n*m)
看复杂度就知道只适用于小图,根据数据量谨慎使用,在没有负权边时要使用Dijkstra算法
Bellman-Ford + SPFA优化的用途:
- 适用于小图
- 解决有负边(没有负环)的图的单源最短路径问题
- 可以判断从某个点出发是否能遇到负环,如果想判断整张有向图有没有负环,需要设置虚拟源点
- 并行计算时会有很大优势,因为每一轮多点判断松弛操作是相互独立的,可以交给多线程处理
注意:
SPFA的另一个重要的用途是解决“费用流”问题,当然也可以被 Primal-Dual原始对偶算法 替代
例题分析:
题目1
Floyd算法模版(洛谷)
测试链接 : https://www.luogu.com.cn/problem/P2910
题目2
Bellman-Ford算法应用(Leetcode)
测试链接 : https://leetcode.cn/problems/cheapest-flights-within-k-stops/
Bellman-Ford算法模板题,但是要注意的是,有可能一轮会更新两次甚至多次距离,导致不足期望次数就更新完毕了。所以要做一个设计:
分开两个表存数据 cur 和 next ,每轮拿数据从cur 拿,保存到 next 中。
题目3
Bellman-Ford + SPFA优化(洛谷)
测试链接 : https://www.luogu.com.cn/problem/P3385
1 | 给定一个 n个点的有向图 |