算法设计-图论相关课堂笔记

为什么需要"时间"这个概念?
DFS 的核心是递归。进入一个节点和离开一个节点是两个完全不同的时刻


返回边:从后代指向祖先
树边:拓展方向
交叉边:两边的节点不存在祖先-后代关系【时间节点不重叠】
前向边:从祖先指向后代(隔代,否则叫树边)
- 树边(Tree edges):在DFS过程中,首次访问一个新节点时所经过的边。它们构成了DFS生成树的骨架。例如,从节点u到其子节点v的边(u,v)就是树边。
- 返回边(Back edges):从一个节点u指向其祖先节点v的边。这类边的存在通常意味着图中存在环。自环(self-loop)也被视为返回边。
- 前向边(Forward edges):从一个节点u指向其非直接子节点的后代v的边。它不是树边,但连接了祖先和后代。
- 交叉边(Cross edges):所有不属于以上三类的边。它们可能连接同一DFS树中无祖先-后代关系的节点,或者连接不同DFS树中的节点。

为什么会有颜色?(节点的三种状态)
在 DFS 过程中,每个节点都会经历三个阶段:
- 白色 :未探索。这个房间我还没进去过,里面有什么完全不知道。
- 灰色 :正在探索。我已经进了这个房间,正在看它的出口,但还没把从这个房间出发能到的所有地方都走完。也就是说,它还在递归栈里,它的"结束时间"还没定。
- 黑色 :探索完成。我已经把这个房间以及从这个房间出发能到达的所有房间都彻底走完了,准备撤退(回溯)了。
为什么颜色可以判断边的类型?
当你站在节点 u ,看向它的邻居节点 v (即检查边 (u,v) )时,vv 的颜色直接暴露了它在搜索树中的身份:
如果 v 是白色
- 含义:你第一次发现这个节点。
- 结论 :树边。
- 解释:既然 v 没去过,那这条边就是你通向新世界的桥梁,它会成为搜索树的一部分。
如果 v 是灰色
- 含义:你遇到了一个"正在进行中"的节点。
- 结论 :返回边。
- 解释 : v 是灰色,说明 v 是你当前的祖先(还在递归栈里)。你现在指向 v ,意味着你从后代指回了祖先,这构成了一个环。
如果 v 是黑色
- 含义:你遇到了一个"已经彻底结束"的节点。
- 结论 :前向边 或 交叉边。
- 解释 : v 已经死透了(处理完了),这条边指向了一个旧世界。
- 如果 v 是你这一支的后代(只是早就处理完了),那是前向边。
- 如果 v 是别人家的节点(或者是你这一支完全不相干的旁支),那是交叉边。
通过(u,v)v的颜色判断边的类型
无向图
只有树边和返回边

在无向图中,边是没有方向的, (u,v) 和 (v,u) 是同一条边。
-
情况 1:第一次遇到这条边
如果你站在 u ,发现 v 还是白色的(没去过),那你就会走过去。
👉 这时,这条边被标记为树边。
-
情况 2:第二次遇到这条边
既然你已经从 uu 走到了 vv ,那么在 vv 的邻接表里,肯定也有一条边指向 uu 。
当你走到 vv 时,你会回头看 uu 。此时 uu 肯定是灰色 的(因为 uu 是你现在的父节点/祖先)。
指向灰色祖先的边,被标记为返回边。
结论 :
因为边没有方向,你要么第一次走它(树边),要么回头走它(返回边)。根本不可能出现"前向边"或"交叉边",因为那需要边有特定的单向性才能成立。
拓扑排序

考点:

利用DFS的性质,巧妙地找出图中所有"环状"或"互相可达"的节点集合 


考查内容

注意要写三样东西,一个是顺序,一个是步骤,一个是边的类型

树
最小生成树
强连通分量,都可互达
克鲁斯卡尔的流程



普利姆的流程


为什么在step2需要(1,2) (0,7)都展示出来,而之后的所有节点都是只对比当前探索节点的最小值,比如step5,就没有引入上一步的的(6,8)


考查松弛问题:
最短路径问题:
单点最短路径

BellFord算法: 能解决负权边
性质


要做V-1次遍历,因为有V-1条边
每一轮要松弛所有的边

要做多少轮?V-1







检测负环是否存在?
更新到第V-1次后收敛,不再更新。
为什么要V-1条边?
因为任意两个节点的最短路径最多包含V-1条边,如果出现环说明两条路必定存在一最短路。
题目:

Bellman-Ford 算法确实是用于解决单源最短路径问题的,特别是当图中包含负权边时。
你提出的关于 有向无环图(DAG) 的问题非常关键,这触及到了算法效率的核心。简单来说,在 DAG 中先进行拓扑排序再执行松弛,是为了**"走捷径"**------将时间复杂度从 O(V⋅E)O(V⋅E) 降低到线性的 O(V+E)O(V+E) 。
为了让你更直观地理解,我为你整理了详细的对比和原理解析:
1. 核心区别:为什么要多此一举做拓扑排序?
Bellman-Ford 算法之所以慢,是因为它**"不知道路的方向"** 。它不知道哪些边是通向终点的,也不知道处理的先后顺序,所以它必须盲目地对所有边进行 V−1V−1 轮重复的松弛操作,以确保最短路径被一步步"传递"到终点。
而在 DAG(有向无环图) 中,我们拥有一个上帝视角:拓扑排序。
- 拓扑排序的作用:它给出了一个线性的处理顺序,保证对于任意一条边 u→vu→v ,节点 uu 一定排在节点 vv 前面。
- 带来的红利 :当我们按照这个顺序处理节点时,一旦处理到节点 vv ,所有能到达 vv 的前驱节点(比如 uu )的最短路径一定已经计算完毕并确定了。
结论 :因为有了拓扑排序这个"导航",我们不需要重复 V−1V−1 轮 ,只需要按照拓扑顺序,把每条边松弛一次,就能得到正确的最短路径。
| 特性 | Bellman-Ford 算法 | DAG 最短路径算法 (拓扑排序+松弛) |
|---|---|---|
| 适用图类型 | 任意有向图(可含负权边) | 仅限有向无环图 (DAG) |
| 核心逻辑 | 暴力尝试:对所有边重复松弛 V−1轮 | 顺序推导:按拓扑序对每条边松弛 1次 |
| 时间复杂度 | O(V⋅E)(较慢) | O(V+E)(较慢) |
| 负权环检测 | 可以检测 | 不需要(DAG 定义上就没有环) |
单源最短路径 Direct Acyclic Graphs(拓扑排序改进法)



Dijkstra 算法
简单来说,虽然"拓扑排序+松弛"在 DAG(有向无环图)中效率极高( O(V+E)O(V+E) ),但它有一个致命的**硬伤,**处理不了"环",而 Dijkstra 算法虽然慢一点( O((V+E)logV)O((V+E)logV) ),却解决了这个硬伤。
1. 最大的问题:它处理不了"环"(通用性差)
这是最根本的原因。
- 拓扑排序的前提 :图必须是无环的(DAG)。如果图中有环(比如 A->B->C->A),拓扑排序根本无法进行(找不到入度为0的节点),整个算法直接瘫痪。
- 现实世界的图 :绝大多数现实生活中的图都是有环 的。
- 地图导航:你可以从 A 走到 B,也可以从 B 走回 A(双向道路)。
- 网络路由:数据包可以在网络中绕圈子。
Dijkstra 的优势 :它不依赖拓扑排序,它通过贪心策略(每次选当前距离最近的点)来处理节点。它完全不在乎图中有没有环,只要没有"负权环",它都能完美工作。
结论 :我们忍受 Dijkstra 稍微慢一点点(多一个 logVlogV ),是为了换取它能处理任意有环图的能力。



总结
| 算法 | 你的总结 | 核心关键词 |
|---|---|---|
| Bellman-Ford | 能解决负权边,有向/无向有环图 | 全能兜底 |
| 拓扑+松弛DAG | 解决有向无环图(无法解决循环依赖) | 极速特化 |
| Dijkstra | 解决非负有环图 | 通用标准 |
每次加入到达根节点s最短的节点,这道题计算的是遍历的节点的顺序

🗺️ 举个简单的例子
假设我们有 A、B、C 三个点,边的权重如下:
- A 到 B 的距离是 5
- A 到 C 的距离是 2
- C 到 B 的距离是 1
- 目标:求 A 到所有点的最短路径。
执行顺序如下:
- 初始化 :
dist[A]=0,dist[B]=∞,dist[C]=∞。 - 第一轮 :
- 找最近:未访问的点中,A 的距离是 0(最小),选中 A。
- 打标记:A 变为"已访问"。
- 松弛邻居 :
- 看 B:A→B 距离是 5。5 < ∞,更新
dist[B] = 5。 - 看 C:A→C 距离是 2。2 < ∞,更新
dist[C] = 2。
- 看 B:A→B 距离是 5。5 < ∞,更新
- 第二轮 :
- 找最近:未访问的点是 B(5) 和 C(2)。C 的距离更小,选中 C。
- 打标记:C 变为"已访问"。
- 松弛邻居 :
- 看 B:从 A 经 C 到 B 的距离是
dist[C] + 1= 2 + 1 = 3。 - 比一比:3 < 5(原本的 distB),所以更新
dist[B] = 3。
- 看 B:从 A 经 C 到 B 的距离是
- 第三轮 :
- 找最近:未访问的点只剩 B(3),选中 B。
- 打标记:B 变为"已访问"。B 没有未访问的邻居了,流程结束。
最终结果:A 到 C 的最短路是 2,A 到 B 的最短路是 3(路径是 A→C→B)。
单元最短路总结
BF法是 遍历所有边松弛 但是复杂度很搞 但是
| 算法 | 你的总结 | 核心关键词 |
| Bellman-Ford | 能解决负权边,有向/无向有环图 | 全能兜底 |
| 拓扑+松弛 | 解决有向无环图(无法解决循环依赖) | 极速特化 |
| Dijkstra | 解决非负有环图 | 通用标准 |
|---|
多源最短路径
APSP Faster All Pairs Shortest Paths
不可以有负环
DP的第一步是找最优子结构

V个节点的图,最短路径的最大长度是V-1.
动态规划APSP解法 动态规划
O(V^4)复杂度太慢了


APSP的简化,由于可以通过A^4 * A^4=A^8因此对于高次幂拆解后的复杂度是O(Vlong(n-1))
Floyd-Warshall 算法(降低复杂度)
对于01背包好用,物品当作节点







Johnson 算法(降低复杂度)
D法只适用于非负权边的图,因此对于有负权边的就必须对每个点跑一遍BF。
考虑将负权边变成非负的边,这样就能用D法了。




可是w(uv)可能是负的啊


总体思路:
将负权边转换为非负权边,引入势能函数,保证负权非负
1.引入超级原点S,将S与所有节点相连,权重为0
2.列出所有边
3.使用Bellmanford进行单元最短路计算,列边松弛列dΠ,得到S到各个点的最短路
4.计算势能 h(v)=δ(s,v)
5.计算等效边权w(u,v)_hat
6.这时就可以迪杰斯特拉了



最多需要遍历最短路径长度的边的轮数,每轮复杂度E





图的应用
流网络问题
流网络就是有向权图

-
第一点:图与容量
- 定义: 流网络 G=(V,E) 是一个有向图。
- 容量: 每一条边 (u,v)∈E 都有一个非负的容量 c(u,v)≥0。你可以把容量想象成水管的最大通过水量。
- 默认假设: 如果两点之间没有边(即 (u,v)∉E(u,v)∈/E ),我们假设其容量为 0。这意味着数据无法直接通过。
-
第二点:源点与汇点
- 源点(Source, s ): 流的起点。定义中提到源点 s 只有出边(outedges),意味着流从这里产生。
- 汇点(Sink, t ): 流的终点。定义中提到汇点 t 只有入边(inedges),意味着流汇聚到这里。
-
第三点:连通性
- 假设: 图中的每一个顶点都必须位于从源点 s 到汇点 t 的某条路径上。
- 意义: 这意味着图中没有孤立的部分,所有的节点都对从 s 到 t 的传输有贡献。
flow是一个动态的属性,实际的流量
、
**最大流问题(Max Flow Problem)**的基础。它建立了一个模型:计算从 s 到 t 最多能流多少水(即著名的 Ford-Fulkerson 算法等)
流量限制:f(u,v)<=c(u,v)
反对称性:f(u,v)=-f(v,u)
流量守恒:V-{s,t}都有f(u,v)=0 中间节点只能传输
对于抵消图,两节点互有输入,因此可以抵消,且不满足反对称性,留下净流量

Ford-Fulkerson Method方法
Ford-Fulkerson 算法的本质就是一个不断寻找增广路径并更新剩余网络的迭代过程。
依赖于三个概念:
剩余网络:每条边还剩余多少流量
增广路:寻找可达路
cuts:如何将网络切割为两部分,限制流量上限
1. 剩余网络(Residual Network)
"这条路还能再走多少?"
- 定义:在原网络的基础上,根据当前的流量情况,构建出一个"还能容纳多少新流量"的网络。
- 核心指标:剩余容量(Residual Capacity)
- 正向边:如果一条水管容量是 10,已经流了 3,那么剩余容量就是 10−3=710−3=7 。代表你还可以往里塞 7 个单位的流量。
- 反向边(精髓所在) :这是 Ford-Fulkerson 算法最巧妙的地方。如果一条边已经流了 3 个单位的流量,算法会人为地增加一条反向边,其剩余容量为 3。
- 为什么要有反向边? 它代表了**"反悔"**的能力。如果你后来发现之前流的这 3 个单位走错路了,可以通过反向边把这 3 个流量"退"回去,重新分配给其他路径。没有反向边,算法很容易陷入局部最优解。
2. 增广路径(Augmenting Path)
"找到一条能走通的路,并灌入流量。"
- 定义 :在剩余网络中,从源点(Source)到汇点(Sink)的一条连通路径。只要这条路径上所有边的剩余容量都大于 0,它就是一条增广路径。
- 瓶颈容量(Bottleneck Capacity) :一条路径能增加多少流量,取决于这条路上最窄的那根管子(即路径上剩余容量最小的那条边)。
- 操作步骤 :
- 在剩余网络中找到一条增广路径。
- 找出该路径上的最小剩余容量(瓶颈)。
- 沿着这条路径,正向边加上这个流量,反向边减去这个流量(更新剩余网络)。
- 重复上述过程,直到再也找不到增广路径为止。
3. 割(Cut)与最大流最小割定理
"流量的天花板在哪里?"
- 定义:割是将网络中的所有顶点分成两个互不相交的集合:包含源点的集合 SS 和包含汇点的集合 TT 。
- 割的容量:所有从 SS 指向 TT 的边的容量之和。你可以把它想象成把网络切成两半,切面上所有水管的总粗细。
- 最大流最小割定理(Max-Flow Min-Cut Theorem) :这是 Ford-Fulkerson 算法正确性的数学基石。
- 定理指出:一个网络的最大流量 = 该网络的最小割容量。
- 直观理解:无论你中间的路网修得多么复杂,最终能流到终点的最大水量,一定受限于整个网络中最薄弱的那个"瓶颈截面"(最小割)。
- 算法终止条件 :当剩余网络中再也找不到增广路径时,说明源点和汇点在剩余网络中不连通了。此时,所有能从源点到达的点构成集合 SS ,其余点构成集合 TT 。这个 (S,T)(S,T) 割的容量恰好等于当前的流量,证明已经达到了理论上限(最大流)

第一行是正向边,减去流量
第二行是反向边,允许退还流量,加回去
同时表达正向容量和反向容量才是完整的当前状态。

计算残差网络



流增广
新流量 = 旧流量 + 正向推流 - 反向退流(要同时对正反边两个状态作出修改)






切割的流


Ford-Fulkerson算法流程
接下来的课程通常会证明:一个网络的最大流量,等于该网络所有可能的割中,容量最小的那个割的容量
最大流最小割定理(Max-flow Min-cut Theorem)
在一个流网络中,从源点 s 到汇点 t 的最大流量,等于将网络分割成两部分(S 和 T)时,所有从 S 指向 T 的边的容量之和的最小值。
简单来说,"最窄的瓶颈"决定了"最大的流量"。

方法

初始化图和剩余图,
找一条通路,找到瓶颈路(),计算原图中的最大flow,讲flow加入原图中,
更新剩余图





时间复杂度

EK法 找最短增广路
增广路径的选择会极大影响复杂度,因此需要一种快速方案
EK法,找最短的路径



算法流程

示例:
不在是在残差图中招任意一条最短路,而是构建一个GL层次图,在层次图里面找一条最短路



当当前的GL层次图没有到t的路径时,就重新在残差图中构建层次图



原图只带几比几、残图有双向边、层图有通也有断
算法复杂度:



匹配问题

匹配就是每个节点只连一条边的边集。
核心定义(文字部分)
- 匹配(Matching): 在一个无向图中,如果你选出一些边,使得图中的每一个顶点最多只连接一条被选中的边 ,那么这些被选中的边就构成了一个"匹配"。
- 通俗理解: 就像相亲配对,一个人只能和一个对象牵手,不能脚踏两只船。
- 已匹配(Matched)与未匹配(Unmatched):
- 如果一个顶点连上了被选中的边,它就是"已匹配"的。
- 如果没连上,就是"未匹配"的。
- 最大匹配(Maximum Matching): 指在所有可能的匹配中,包含边数最多的那个匹配。
- 完美匹配(Perfect matching): 指图中的每一个顶点( VV 中的所有点)都被匹配上了

- 最大匹配不一定是完美匹配: 你可能已经尽力配对了(没法再加边了),但还是有落单的人(如右图所示)。
- 完美匹配一定是最大匹配: 如果所有人都配对成功了,那肯定已经是数量最多的情况了,不可能再多了。
交错路径(Alternating Path)

定义:一条简单路径,其边在"匹配边"和"非匹配边"之间交替出现。
通俗解释:想象你沿着图中的一条路走,你走的每一步,必须是"选中边"和"未选中边"交替进行。
作用:它是构建增广路径的基础。算法通过寻找特定的交错路径来改进当前的匹配。
增广路径(Augmenting Path)

定义:一条非平凡的交错路径,它的起点和终点都是"未匹配顶点"(即没有连任何粗边的顶点)。
为什么它如此重要?
增广路径是改进匹配、增加匹配边数的唯一途径。它的核心思想是"翻转"路径上的边:
- 路径结构:因为起点和终点都是未匹配点,所以路径的第一条边和最后一条边必定是"非匹配边"。
- 翻转操作:将路径上所有的"非匹配边"变为"匹配边",同时将所有的"匹配边"变为"非匹配边"。
- 结果 :经过翻转后,匹配边的总数会增加1。因为路径以非匹配边开始和结束,所以翻转后,匹配边的数量比非匹配边多一条。
核心定理(Berge's Lemma):一个匹配是最大匹配,当且仅当图中不存在关于该匹配的增广路径。
总结:
- 交错路径是"路"。
- 增广路径是"能让我们变得更好的路"。
- 算法的目标就是不断寻找增广路径,通过"翻转"操作来增加匹配数,直到再也找不到增广路径为止,此时的匹配就是最大匹配。
核心操作:减与反转



- 内容: 所有的理论都指向一个结论:一旦你找到一条增广路径,把它"加"到当前匹配中(通过异或/翻转操作),你就能得到一个更大的匹配。

算法流程

二分图

核心定义(上半部分文字)
- 定义: 一个无向图 G=(V,E)G=(V,E) 是二分图,当且仅当它的所有顶点集合 VV 可以被切分成两个不相交的子集 LL (左边)和 RR (右边)。
- 关键规则:
- 内部无连接: LL 里面的点互不相连, RR 里面的点也互不相连。
- 跨组连接: 每一条边 ee 都必须是一头在 LL ,另一头在 RR 。
最大流算法

最大匹配问题可以使用最大流算法

它展示了如何把左边的二分图 ,变成右边的流网络
- 我们想求左图能连出多少对线(最大匹配)。
- 我们把它变成右图,然后跑一遍最大流算法(比如 Ford-Fulkerson 算法)。
- 算出来的最大流量是多少,左图的最大匹配数就是多少
max_matching和max_flow是等价的。

必须用1为最大流量,才能限制1对1匹配/
匈牙利树算法
从左侧节点中找到一个未匹配的节点,以此生成多条augpath,要求终点是unmathced的,然后翻转,更新M={()()}(加一),


算法流程

M={}
从空集开始,从l1开始匹配,找augpath,也就是找未匹配的终点,r1 r2 r4都可以。

M={(l1,r1)}
从l2开始匹配,找augpath,也就是找未匹配的终点,r2 r3都可以。

M={(l1,r1),(l2,r1)}
从l3开始匹配,找augpath,也就是找未匹配的终点,r3 r4都可以。

M={(l1,r1),(l3,r1),(l1,r4)}
从l4开始匹配,找augpath,也就是找未匹配的终点,r3可以。

做小题怎么办

匈牙利算法既可以从空集(没有任何匹配边)开始,也可以从任意一个合法的初始匹配(只要这些边互不冲突)开始。无论你的起点是什么,只要按照算法逻辑不断寻找并反转增广路,最终都能达到最大匹配。
为什么可以从任意合法匹配开始?
这背后的根本原因依然是增广路定理(Berge 引理):一个匹配是最大匹配,当且仅当图中不存在任何增广路。
- 殊途同归:算法的终点是由"图中是否还存在增广路"决定的,而不是由"起点"决定的。
- 动态调整:如果你随机给定的初始匹配比较"笨拙"(占用了关键节点,导致后面的人没得选),匈牙利算法在后续为其他节点寻找增广路时,会通过递归回溯(也就是你提到的"匈牙利树"的生长过程)去尝试"腾位置"。如果当前占位的人能找到别的下家,他就会让出位置;如果实在腾不出来,算法才会放弃。这个过程会自动修正前期不合理的匹配,直到再也找不到增广路为止。
线性规划


常见定义


矩阵化表达

有m个约束 n个变量。
要标准化转为线性约束标准问题

要求:所有x大于0 Σax<=b max问题
要把min变成max
要把=改为<=
要把所有的x约束大于0
1 将min改为max问题

2 将等式变为两个不等式夹击

3 并通过同乘符号把大于等于>=改为小于等于<=

4 补充限制条件
将自由变量变为两个非负变量的差
要求xj xj都大于0,这样就可以表示任意的实数

算法希望处理等式约束。
松弛形式
1 引入松弛变量 将不等约束改为等号约束
s定义为冗余量,即满足条件的剩余空间
Ax<=b => Ax+s=b

引入松弛变元------冗余量
引入后变量的总数会等于 不等号*2+等号 约束条件

basic nonbasic变量
约束等号左侧的变量x4 x5 x6是basic变量
x1 x2 x3是等号右边的,是nonbasic变量

2 将目标函数max也改为等号形式

将目标函数和下面的约束改为了等式。
这样等号左侧都是basic变量,右侧都是nonbasic变量。
(因为要分离BN变量,因此必须做将等号拆为不等式,再引入松弛变量转为等式的过程)
这样就可以转为矩阵形式运算。

A约束矩阵 b常数项 c目标项

LP的应用
将最短路问题用LP解决


将最大流问题转为LP问题

如何解决LP问题------SImplex算法
初始化时所有NB变量都为0,此时可以计算出B变量的值 和 z的值

为了让Z最大,因此要增加x1 x2 x3
从三个NB变量中选一个,从零增加
这里选择x1
分别带入三个约束式子,看看在满足大于零1约束下要多少。
选择最少的x1.
x1《=9

然后把x1当作NB变量,把x6(交换过来的式子的NB变量)放到右侧(交换NB)
然后修改目标函数和约束条件(x1变为NB x6变为B变量)

然后基于这个新问题继续做
增加x2或x3,这里增加x2



选择x3更新


更新x2,由于x4=x2,因此增加x2不会导致x4的约束被破坏。

当更新到z的式子后面的参数都是负号时,说明增加谁都无法增加z了,停止更新。

算法流程
STF+RLF
初始0,选正参。(初始化所有的参数,N参数为自己,B为0;对于z=+-+-中+的参数更新)
最小正满足(在满足N变量大于0的情况下,计算让B最小的条件)
交换NB(代入式)(根据这个约束行,计算B=xxxxN,交换NB;并带入其他涉及B的约束式)
更新所有的代入处(更新z后,看z=后面是否全负,全负停止)
图形化理解
实际上就是抓住一个边,在这个边上移动,当到达交点的时候,就交换约束和移动方向。
每次交换都需要改变坐标轴