今天是学习打卡的第53天。按理说,我应该把“图论”这个阶段收个尾,整理完笔记就切入下一个专题了。但翻热词的时候看到“图论”相关搜索热度一直没下去,甚至“图论中图的直径怎么算”“图论与网络最优化算法pdf”“图论及其应用张先迪课后答案”这些长尾问题都涌上来了。我干脆把原计划打乱,用这完整的一天把图论里最容易让人卡壳、也最容易被面试官拿来“炒冷饭”的几个点捋了一遍,顺便把那些学的时候觉得“这辈子用不上”、后来真在做业务时被反复打脸的内容也重新看了看。
这篇文章不是那种把《图论及其应用》从第一章抄到最后一章的知识点汇总,那样太无聊了,网上随便一搜都是。我这一篇更想解决的是三类人的痛点:第一类是刚开始学图论、被各种定理绕晕的初学者;第二类是刷LeetCode刷到图就发怵、想建立体系感的选手;第三类是工作中偶尔要跟图数据打交道、想快速找回建模感觉的工程师。下面所有内容都是我以自己的踩坑经历、教材使用心得、实际跑代码后的反思为主线写的,希望能给你省一点时间。
1. 从热搜问题切入:图的直径到底怎么算
1.1 直径不是“图的对角线”,而是最远最短距离
先说这个热搜问得最多的“图的直径”。很多人第一反应是“找两个最远的点,然后连一条线量长度”,这个直觉方向是对的,但表述不够严谨。图的直径的定义是:图中所有顶点对之间的最短路径长度的最大值。也就是说,你先要算出任意两点之间的最短距离,然后再从这一堆最短距离里挑出那个最大的值,才是直径。
举个例子,如果有一个简单链状的图,顶点是 A-B-C-D,A 到 D 的唯一路径长度是 3,那这个图的直径就是 3。但如果 A 和 D 之间还多了一条直接相连的边,构成一个环状四边形,那么 A 到 D 的最短路径就从 3 缩短成了 1,整张图的直径也会跟着变成 1。从“唯一路径”到“多了一条捷径”,图的直径发生了戏剧性变化。所以,算直径时最忌讳的就是看到两个点之间有边就直接用这条边的长度,而忽略了其他更短路径的可能性。这里的核心是“最短路径”这四个字。
我习惯用一个生活类比来理解它:把图想象成一张地铁线路图,每个站点是一个顶点,线路是边。所谓直径,就是这张地铁网里任意两个站点之间最快到达时间的“最大值”。如果一座城市的地铁换乘设计得很糟糕,那直径就会很大,意味着可能有一段行程要绕特别远;反之,如果设计得好,站点之间很快就能互相到达,直径就会很小。这个指标不是用来衡量某一对用户的速度,而是衡量整张网络的“最坏情况下的效率”。
1.2 三种实用解法与复杂度对比
那具体怎么算直径呢?针对不同图的规模和边的权重情况,我整理了三套思路,你按需取用就行:
| 方法 | 适用场景 | 时间复杂度 | 实现复杂度 |
|---|---|---|---|
| 逐点 BFS | 无权图、边数较少的稀疏图 | O(V × (V + E)) | 低,只需要会 BFS |
| Floyd-Warshall | 有权图、顶点数量较少(V ≤ 500) | O(V^3) | 中,三重循环 |
| Johnson 算法 | 有权图、稀疏图、含负权边 | O(V^2 log V + VE) | 高,要套 Dijkstra |
对于小白来说,我建议先把第一种吃透。每个顶点做一次 BFS,就能得到它到所有其他顶点的最短距离,然后把全局最大值找出来。这个方法看着憨憨的,但胜在思路直接,基本不会写错。如果图的边权都是非负数,顶点数几百个以内,用 Floyd-Warshall 反而更好写——三重循环,先初始化距离矩阵,然后不断尝试“经过某个中间点是否能缩短距离”,最后扫一遍矩阵取最大值,完事。
我自己在笔记里存了一个手写的无权图 BFS 算直径的模板,Python 大概长这样:
from collections import deque def graph_diameter(adj): n = len(adj) max_dist = 0 for start in range(n): dist = [-1] * n dist[start] = 0 q = deque([start]) while q: u = q.popleft() for v in adj[u]: if dist[v] == -1: dist[v] = dist[u] + 1 q.append(v) # 更新全局最大距离,同时处理非连通图的情况 cur_max = max(dist) if cur_max > max_dist: max_dist = cur_max return max_dist注意:如果图是非连通的,dist 里会存在 -1,这时严格来说直径是无穷大。竞赛里通常会在输入里保证连通性,但实际业务图数据可不一定,所以我在代码里只对 dist 中非 -1 的值求 max,这样就绕开了“非连通导致错误结果”的问题。这个细节,教材里不会教,实际排查 bug 时却很容易踩。
1.3 为什么大家都来搜这个问题
热度这么高,我猜有几个场景。一是张先迪老师的《图论及其应用》里,直径、半径、偏心集这一节是很多院校图论课程的作业题源,课后题特别喜欢出“请计算下图中各点的偏心率,并找出图的直径和中心”。二是在算法面试中,“图的直径”经常被包装成实际问题来考,比如“你有一张社交网络关系图,消息从一个用户传到邻居需要一轮,那全网传播至少需要多少轮”,解题思路本质上就是算图的直径。第三个场景,也是我自己工作里真实遇到的:在设计一个分布式缓存系统时,为了估算多级缓存之间的数据同步延迟上限,我把缓存节点之间的网络延迟抽象成一张加权图,算出的直径就是最坏情况同步延迟。这种“最坏情况上限”的思维,在系统设计里其实非常值钱。
2. 学图论不能只盯着算法看,先理清这几条主线
2.1 图的存储结构:邻接矩阵还是邻接表?
很多新手一上来就背算法模板,结果连图怎么存储都没想明白,写出的代码不是内存超限就是遍历顺序混乱。我建议在动手前,先把这三个最基础的问题想清楚:顶点和边分别代表什么;边有没有方向;边上有没有权重。想清楚之后,存储方式就很好选了。
- 邻接矩阵:用一个 V×V 的二维数组存边权,优点是判断两个顶点之间是否有边是 O(1) 的,Floyd 算法用起来尤其顺手。缺点是空间是 O(V^2),V 超过一万就非常吃力。
- 邻接表:每个顶点挂一个链表或数组,只存与它直接相连的邻居。优点是省空间,BFS/DFS 遍历时开销也小,绝大多数竞赛题和面试题推荐用这种。缺点是判断“u 和 v 是否相邻”需要遍历一遍列表,但实际场景中这个操作频率并不高。
- 链式前向星:说实话,如果不是在打 ACM 或者跑超大规模图,我建议普通学习者先别碰。它确实快,但抽象程度太高,容易劝退。
我的个人习惯是:刷题为主就无脑邻接表;涉及 Floyd 或稠密图就邻接矩阵。工程里如果用的是 Python,我经常会直接借助邻接表加一个字典来存边权,比如adj[u].append((v, weight)),兼容性很强。
2.2 树的特化性质,是你理解图论的捷径
树是图的一个特例,但正因为特化,它有一堆好用得惊人的性质。我当时记第一个性质时特别有印象:一棵有 n 个顶点的树,恰好有 n-1 条边。你拿这个性质去判断一个图是不是树,一句话的事。反过来,如果一个连通图有 n 个顶点和 n-1 条边,那它就是树,不可能有环。
第二个性质是:树里任意两个顶点之间有且仅有一条简单路径。这意味着在很多问题里,树结构可以把“找路径”从复杂的搜索问题降维成“找最近公共祖先”的问题。我在刷二叉树题目时积累的手感,放大到更一般的树结构上一样适用。然后是生成树问题,Kruskal 和 Prim 两种贪心算法,前者适合边少图(稀疏图),按边权从小到大排序然后用并查集判断是否成环;后者适合点少但边特别多的稠密图。实际选举哪种,不用背,你只要记住一句话:稀疏图用 Kruskal,稠密图用 Prim。我学的时候走了弯路,总是强迫自己两个模板都默写一遍,后来才意识到先用数据规模判断再选算法才是最高效的。
2.3 二分图、连通分量与强连通分量,构成了图论的判断家族
在刷题和面试里,“给你一张图,问一些性质”这一类问题,其实翻来覆去就考这么几样:
- 是不是二分图:用染色法,从一个点开始 BFS/DFS,给相邻点染相反颜色,如果出现冲突说明不是二分图。二分图最经典的应用就是匹配问题,比如“给 N 名员工分配 M 个岗位,每个员工能胜任若干岗位,如何让尽可能多的员工都上岗”,建模成二分图最大匹配就行。
- 有几个连通分量:无向图里,从任意未访问顶点开始 DFS,一次遍历能覆盖到的就是同一个连通分量。社交软件里的“你可能认识的人”推荐、后端服务里“不同网络区域是否可达”的判断,背后都是连通分量思想。
- 有向图里的强连通分量:最常用的是 Tarjan 算法。它把所有互相可达的顶点缩成一个点,最终形成一张有向无环图(DAG)。一旦图变成了 DAG,你就能用拓扑排序处理问题,比如“哪些模块必须先编译”“哪些服务之间没有循环依赖”。这里我想多说一句:很多工程师对 Tarjan 有畏难情绪,但它真的不难,核心是维护一个栈和一个时间戳数组,理解递归回溯时的栈帧变化,再配合一到两个例子手推一遍,就通了。这类算法第一次学的时候慢一点完全没关系,关键是搞懂原理,而不是背模板。
3. 教材搭配方案:张先迪、网络最优化算法资料如何配合使用
3.1 张先迪《图论及其应用》该怎么读,才不会被证明劝退
这本书名气很大,很多学校直接拿它当教材。但我观察到一个普遍现象:初学者翻开第一章,看到“图、简单图、多重图”等概念还好,再看到后面各种定理的严格证明,就受不了了。我的意见是:这本书更适合当案头工具书,而不是从头到尾的“刷书”对象。第一遍学习时,你把基本概念、握手定理、树的性质、连通性、图的矩阵表示这些章节认真过一遍就好;到了匹配理论、Ramsey 定理这种偏理论深度的章节,可以先看懂定理表述和结论,证明过程留到需要深挖时再回来啃。
很多人搜“图论及其应用张先迪课后答案”,我的建议是先自己推导,实在卡住再看答案。图论题的答案经常是“点睛之笔”,比如构造法证明题里那一个巧妙的构造,看答案前想三天,看答案后恍然大悟。但如果你永远直接看答案,你永远培养不出“自己构造”的脑回路。我在打卡的第 47 天,就是纯靠自己画图推了一个关于树的重心的性质的题,推了一个多小时,虽然过程磕磕绊绊,但效果远好于直接抄答案。这一点我很确定。
3.2 用《图论与网络最优化算法》补齐应用视角
如果只读张先迪那本书,你会觉得图论是一门偏数学的学科。但加上网络最优化算法的资料后,整个视角就变了。像最短路、最大流、最小费用最大流、最小生成树这些“能立刻用代码跑出结果”的算法,才是图论在工程和竞赛中发光的核心。
我用的是电子版 PDF,它的章节安排里,网络流、匹配、整数规划等内容都给出了算法步骤和实例,和教材的证明形成互补。我的阅读顺序是:先看最短路,再看生成树,然后再啃最大流。最大流这块很多人第一次学会有点懵,我建议一定要亲手画一遍残量网络。你真去画了,就会发现增广路其实就是“从源点到汇点还能找到一条容量为正的路径”的过程。这种从抽象到具体的转化,光靠看 PDF 是不行的,必须配合动笔。
3.3 一天的时间线:我 Day53 是怎么安排的
为了给也同样在坚持打卡的朋友一个参考,我把这一天的学习安排列出来:
- 上午(大约 2.5 小时):集中看张先迪教材中直径、半径、中心相关的章节,同时把邻接矩阵、邻接表两种存储方式的手写实现都过了一遍,因为后面所有算法都建立在存储之上。
- 下午(大约 3 小时):打开平时刷题的网站,完成三道图论题。一道是“求无权图的直径”,我故意不用 networkx 里现成的
diameter()函数,而是自己 BFS 实现了一遍,这样理解才足够深。第二道是“判断二分图”,用染色法手写。第三道是“最小生成树”,用 Kruskal + 并查集实现。 - 晚上(大约 2 小时):把今天犯的错、之前的疑惑统一整理成一篇笔记。比如,我发现自己在 Floyd 算法里经常会漏掉“中间顶点 k 必须放在最外层循环”这个细节,就在笔记里用红字标注,并配了一个错误例子说明为什么内层循环不行。
说句实话,一天 7 小时左右的状态对我来说是常态,但并不是每天都能保持。如果你时间有限,你可以把上午压缩到 1 小时,重点看看定义和例题;晚上再抽半小时做题,效果也不会差太多。学习图论这种知识密度高的主题,短时间高强度的“沉浸式”学习,比每天只摸十分钟要高效得多。
4. 几个无论如何都应该亲手写一遍的图算法
4.1 BFS 和 DFS,是所有图算法的地基
BFS 和 DFS 看似简单,但很多复杂算法都是从它们衍生出来的。BFS 的特点是逐层向外扩展,天然自带“最短路径”属性,所以无权图的最短路用它算最简单。DFS 的特点是沿着一条分支走到底,特别适合检测环、计算连通块、处理回溯类问题。
我写过一个很典型的 BFS 求无权图最短路径的模板,差不多是下面这样:
from collections import deque def bfs_shortest_path(adj, start, target): n = len(adj) dist = [-1] * n dist[start] = 0 q = deque([start]) while q: u = q.popleft() if u == target: return dist[u] for v in adj[u]: if dist[v] == -1: dist[v] = dist[u] + 1 q.append(v) return -1 # 不可达这个模板在刷题时几乎可以直接套用到“单词接龙”“打开转盘锁”这类问题上,换汤不换药。你要注意的是:dist数组同时起到了“访问标记”和“记录步数”的双重作用,省掉单独开一个 visited 数组的开销。这种小优化写多了就变成习惯,代码也会清爽很多。
4.2 最短路径三兄弟:Dijkstra、Bellman-Ford、Floyd
这三兄弟我每次讲到都忍不住提醒一句:千万别把它们的适用范围搞混了。
- Dijkstra:处理边权非负的单源最短路,贪心思想 + 优先队列优化后是 O((V+E) log V),是面试和竞赛中的主力。为什么它不能处理负权边?因为 Dijkstra 每次贪心取出当前距离最小的点,并认为这个点的距离已经确定不再更新。一旦有负权边存在,可能出现“某个点被标记为已确定后,后来通过一条负权边变得更短”的反例,贪心前提就崩了。这个例子我建议你自己画一个,只有亲手推一遍才会真正信服。
- Bellman-Ford:可以处理负权边,还能检测负权环。思路是“对所有边松弛 V-1 轮,每轮至少有一条最短路径的边数加 1”。效率不咋地,但胜在鲁棒。SPFA 是它的队列优化版,大部分情况下跑得快,但在最坏情况下复杂度可以退化,所以竞赛里如果出题人想卡你,SPFA 是可能被卡掉的。
- Floyd:全源最短路,代码短、思路直白,三重循环完事,但复杂度 O(V^3)。如果顶点数量在 500 以内,完全可以直接用它,不用折腾 Johnson 算法。
我在写代码时有一个习惯:只要问题里没有明确说“边权有负数”,我就默认用 Dijkstra,因为它在正权图上表现最稳定。只有出现负权边,我才切换到 Bellman-Ford。
4.3 网络流:从最大流到二分图最大匹配的建模思路
网络流这一块其实是图论里特别迷人的部分。最大流的核心就是 Ford-Fulkerson 方法:不断在残量网络里找增广路,直到找不到为止。E-K 算法就是 BFS 找最短增广路,Dinic 算法通过分层图实现多路增广,效率更高。面试里考网络流的不多,但竞赛里很常见,而且最大流有一个特别漂亮的建模技巧:二分图最大匹配可以转化为最大流问题。
具体做法是:源点连向左部所有顶点,容量为 1;左部顶点连向右部可匹配的顶点,容量为 1;右部顶点连向汇点,容量为 1。在这个网络上跑一遍最大流,最大流的值就是最大匹配数。我第一次看到这个转化时真的被惊艳到了:原来“人和岗位的匹配”这种问题,居然能用水流来模拟。如果不想引入太复杂的网络流模板,二分图匹配也可以用匈牙利算法,代码短很多,思路是“让出一个位置给新来的人,自己再去找别的”的回溯逻辑,学有余力的可以两个都掌握。
5. 把图论落到真实场景:建模往往比会背算法更值钱
5.1 地图导航与网络路由里的图论
地图导航是图论最朴素的应用场景。地图上的路口是顶点,道路是边,红绿灯、限速等条件折合成边权,导航就是从起点到终点的最短路径问题。很多人以为导航用的是 Dijkstra,但实际上更常见的是 A* 算法。A* 在 Dijkstra 的基础上引入了一个启发式函数(比如直线距离或估算时间),让搜索方向更快朝着终点靠拢,从而减少无效搜索。这背后的思想其实很简单:如果我们知道终点大致在哪个方向,就没必要把整张地图都展开。
图的直径在导航场景下的意义也很有意思。你算出一张城市路网的直径,就相当于知道“这座城市从一头到另一头最坏需要多久”。这个数值在服务端有实际用途:比如做限流或超时配置时,如果城市级路网的上限时间已知,那么给用户端的请求超时时间至少要大于这个直径,否则就会误杀合法的长行程请求。类似思路,在 CDN 缓存系统里也成立。
5.2 社交网络分析与推荐系统里的中心性
在做社交网络分析时,图论的中心性指标比“图的直径”这个名字要常用得多。度中心性看的是这个点跟多少人直接相连,直观但容易偏向“广撒网”的节点。紧密中心性看的是该点到其他所有点的平均最短距离,越小的点越在“信息传播的中心”。介数中心性看的是有多少条最短路径经过了该点,它最能反映“关键枢纽”角色,但计算量也最大。
有意思的是,图的直径在这些指标计算中扮演了一个“边界角色”:图的直径越大,信息要传到全网需要经过的轮数上限就越大,这直接影响传播模型的调参。比如迅雷下载中的节点发现、P2P 网络中的广播扩散,都要考虑“网络直径带来的延迟上限”。如果你是在大厂做社交推荐,这些概念大概率会以某种形式出现在需求讨论中,早学早受益。
5.3 依赖编排与 DAG:图论给工程世界最重要的礼物
我个人认为,工程实践里最有价值的图结构是 DAG,也就是有向无环图。编译系统在编译前需要知道每个源文件依赖哪些头文件,这就是一个 DAG;持续集成流水线里,任务 A 完成后才能跑任务 B,这又是一个 DAG;甚至你早上起床后的“做早餐”步骤,煮咖啡、热牛奶、烤面包,也可以画成一个 DAG。
处理 DAG 的经典算法是拓扑排序,输出的顶点顺序满足“所有边都从前往后”。如果你发现拓扑排序无法覆盖全部顶点,那说明图中存在环,可能意味着某个依赖链出现了循环引用。我在实际工程排查中就用过一次:一个微服务依赖关系图里,A 依赖 B、B 依赖 C、C 又反向依赖 A,导致线上启动时服务一直互相等待。当时用拓扑排序检测出这个环之后,问题立刻定位了。这种“图论直接救项目”的成就感,远比刷题时 AC 三道题来得强烈。
6. 写在第 53 天结束前的话
如果你也在坚持每日打卡,稍微有一点自己的小体会想分享:图论这门课,最忌讳的是“看得多、写得少”。图论的算法和数据结构不同,它极度依赖直觉和动手能力,很多题你看答案觉得“啊,原来如此”,但自己独立写的时候完全不知从何下手。我的解决办法是:每学一个新算法,就必须在纸上画至少一个例子,再手写一遍不查模板的代码。这个过程很慢,但很值。
还有一个具体的小建议:把图论的笔记按“问题类型”而不是“算法名称”来组织。比如“如何判断两点是否连通”下面,不只写 DFS/BFS 的模板,还要写并查集的做法;在“如何找环”下面,既写 DFS 的遍历标记法,也写拓扑排序检查剩余节点的方法。这样当你遇到一个新题时,你不是翻开书找算法,而是先判断这个问题属于哪一类,再选择顺手的方法。这种思维方式,我认为是这个主题带我走向“内化”的关键一步。
最后,如果你学到这里,不妨用下面三个问题自测一下:第一,给定一个含 4 个顶点的环,你能算出它的直径是多少吗?第二,一张有 6 个顶点的连通图,边数最少是几条?最多是多少?第三,用 BFS 实现一个“判断二分图”的函数,你能在五分钟内写出来吗?如果都能答上来,说明你这一天的图论学得很扎实。如果你之前被图论的证明劝退过,不妨换个思路,先从“为了解决问题而学算法”出发,再回头看书里的定理,你会发现以前觉得难的东西,突然就变得能看懂了。