想象银河城的居民住在六个小岛上,岛与岛之间用桥连接,每天通勤的人流在桥上穿梭。市长想给城市划分社区,可手头没有地图;他只知道岛、桥和桥上的人流。这时,一位聪明的规划师提出:找出那些大家最离不开的桥——也就是每天必经人次最多的桥——然后拆掉它。桥一断,城市自然裂成几块“小岛群”;再找出新格局下最拥挤的桥,继续拆。拆到某个程度,剩下的岛群紧密得无法再分,那些岛群就是社区。这个朴素想法,就是Girvan-Newman算法:反复移除边介数最高的边,让网络自己显露出社区骨架。本文将带你从“边介数”这个概念起步,逐步理解算法如何画出一棵“拆桥族谱”,用模块度判断何时收手,最后用Python亲手实践。

岗位关联

掌握社区发现经典算法,能让你在社交、生物与组织网络分析中快速识别隐藏群体,是图算法工程师的核心技能之一。

目录
  1. 可视化演示
  2. 社区发现目标
  3. 边介数原理
  4. 分裂式流程
  5. 模块度指标
  6. 复杂度剖析
  7. 工程实战
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

通过边介数计算、逐步移除高介数边和模块度评估,直观展示GN算法发现网络社区结构的完整过程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

边介数中心性 边介数=所有节点对最短路径经过该边的次数 连接社区的边通常有最高介数 EB(e) = Σ s≠t σst(e) / σst GN算法每轮移除介数最大的边

社区发现目标

银河城最初没有社区地图,只有岛屿和桥。市长想要划分社区,但社区边界并非天生存在;一群人往来频繁就会形成小团体,这与地理位置无关。从图的视角看,每个小岛是一个节点,桥是一条边,频繁联系自然表现为密集的边。社区发现要回答的问题正是:能否只凭这些“节点和桥”,自动找出一个个内部连接紧密、外部连接稀疏的岛群?

在数学上,这种小团体称为社区结构。社区发现的任务就是找到一个划分,使社区内部边尽可能多、社区之间边尽可能少。它不同于“按指定条件切割”,因为划分往往不唯一:同一张网络可能有多种切法,割在正中还是偏左都会得到不同结果,所以需要算法自动寻找相对合理的划分。

技术上,网络可以表示为无向图 $G=(V,E)$。社区划分 $\mathcal C$ 是一组节点的子集,要求不重叠、全覆盖。Girvan-Newman 算法会不断生成候选划分,最终要靠质量指标来决定取舍,这也是后面模块度登场的原因。

银河城完成划分后,每个岛群内部桥多、群间桥少,这就是“社区”的直觉。下一步的关键是:如何找到那些决定边界的“繁忙桥”?

G=(V,E),C={C1,…,Ck},⋃iCi=V,  Ci∩Cj=∅ (i≠j)\displaystyle G=(V,E),\quad \mathcal C=\{C_1,\ldots,C_k\},\quad \bigcup_i C_i=V,\; C_i\cap C_j=\emptyset \ (i\neq j)
通俗说明:把银河城抽象成一张由节点和边组成的地图,社区划分就是把岛上居民分成若干个互不重叠的“岛群”。
  1. 节点集合 $V$表示图中所有对象,比如银河城里的每个小岛。
    V={v1,v2,…,vn}V=\{v_1,v_2,\ldots,v_n\}
  2. 边集合 $E$表示对象之间的关系,每座桥就是一条边。
    E={e1,e2,…,em}E=\{e_1,e_2,\ldots,e_m\}
  3. 划分 $\mathcal C$将节点分成 $k$ 个小组,小组之间没有交集,并起来等于全部节点。
    C={C1,…,Ck},⋃iCi=V, Ci∩Cj=∅\mathcal C=\{C_1,\ldots,C_k\},\quad \bigcup_i C_i=V,\ C_i\cap C_j=\emptyset

边介数原理

规划师在拆桥前会先统计每座桥的人流。具体来说,他先看任意两个小岛之间的“必经之路”,再数每条必经之路上有多少对居民要使用某座桥。一座桥承载的“必有路径”越多,它就越可能是社区之间的咽喉。这个思路把社区边界问题转化成了桥的负载问题。

图论里,把“必经之路”严谨化为最短路径。对无向图,两个节点之间的最短路径可能不止一条,比如从 A 岛到 D 岛可以走两条长度相同的路线。边介数定义了一条边被多少条最短路径经过的比例:如果一座桥出现在许多节点对的最短路径上,它在网络中就是绕不开的“咽喉”。

计算边介数可使用广度优先搜索(BFS):从每个源点出发,记录最短路径数量和前驱节点,再按层次反向累积流量。边介数高的边位于许多节点对的必经之路上,往往是连接社区的桥梁。这也是 Girvan-Newman 选择拆除对象的核心依据。

银河城的规划师先统计出所有桥的人流,最拥挤的桥就是第一个拆除对象。这个“人流”指标,就是让 Girvan-Newman 算法找到社区边界的核心力量。

B(e)=∑s≠tσst(e)σst\displaystyle B(e)=\sum_{s\neq t}\frac{\sigma_{st}(e)}{\sigma_{st}}
通俗说明:$\sigma_{st}$ 是从 $s$ 到 $t$ 的最短路径条数,$\sigma_{st}(e)$ 是其中经过边 $e$ 的条数;把所有起点/终点组合累计,就得到桥上的“人流”。
  1. 最短路径计数 $\sigma_{st}$从节点 $s$ 到节点 $t$ 的最短路径一共有几条。
    σst\sigma_{st}
  2. 经过边 $e$ 的计数 $\sigma_{st}(e)$这些最短路径里,有多少条会走过桥 $e$。
    σst(e)\sigma_{st}(e)
  3. 比值累加对所有不相同的节点对求和,比例越高说明 $e$ 越繁忙。
    ∑s≠tσst(e)σst\sum_{s\neq t}\frac{\sigma_{st}(e)}{\sigma_{st}}

分裂式流程

拆桥不是一次性完成。最拥挤的桥拆掉后,原本被它隔开的人们会重新规划路线,桥上的“人流”也会改变。因此每拆一座桥,都要重新统计一遍。这个“拆了再算、算了再拆”的循环,正是 Girvan-Newman 算法的骨架。

Girvan-Newman 是一个迭代循环:计算当前所有边的介数,找到最大的那根移除,然后重新计算。每移除一条边,原本连通的网络就可能裂成更多连通分量。这里有一个容易被忽略的关键:边介数只在“当前剩下的图”上计算,而不是沿用原始网络的结果,因为拆桥后最短路径已经变了。

记录每次移除边后得到的连通分量,就形成了一棵分裂树。树根是最初的完整网络,叶子是单个节点或最小社区;中间每一层对应一次拆桥后的划分。要选择哪一层作为最终结果,需要借助模块度指标,而不是机械地拆到底。

银河城每拆一座桥就记录一次新地图,一连串地图摞起来,就形成“拆桥族谱”。但拆到什么时候才合理?这需要听一听居民的“满意度”——模块度。

et∗=arg⁡max⁡e∈EtB(e),Gt+1=Gt∖{et∗}\displaystyle e^*_t=\arg\max_{e\in E_t}B(e),\qquad G_{t+1}=G_t\setminus\{e^*_t\}
通俗说明:每一步都挑当前最拥挤的桥拆掉,拆桥后的新地图进入下一轮。
  1. 当前边集 $E_t$第 $t$ 轮还未拆除的桥的集合。
    EtE_t
  2. 选择最繁忙边 $e^*_t$在 $E_t$ 中找边介数最大的桥。
    et∗=arg⁡max⁡e∈EtB(e)e^*_t=\arg\max_{e\in E_t}B(e)
  3. 移除更新把选中的桥从图中删去,得到下一轮的网络。
    Gt+1=Gt∖{et∗}G_{t+1}=G_t\setminus\{e^*_t\}

模块度指标

如果规划师一直拆,最后每个小岛都孤立,这显然不是社区。于是居民满意度被引入:同一社区内的人是否比随机情况下更爱来往?若社区内外差别明显,满意度就高;若拆出来的小组和随机拼凑没区别,满意度就低。这个满意度,就是模块度。

模块度比较真实网络的连边数与“保持各节点度数不变、边随机重连”的期望连边数。若实际内部边明显多于期望,则该划分有社区结构。它像一把尺子:每个候选划分都能量出一个分数,分数越高,说明“社区感”越强。

模块度取值通常在 $[-1,1]$,大于 $0.3$ 说明社区结构明显。GN 算法会生成多个层次的划分,每一层可以计算一个 Q 值,选择 Q 最大的一层作为最终社区划分,这就是“在分裂树上取峰值”的直觉。

银河城居民满意度最高的那一层,就是拆桥计划的“最佳停手点”。既不过粗地所有人一团,也不过细地拆散每一座岛。

Q=12m∑ij[Aij−kikj2m]δ(ci,cj)\displaystyle Q=\frac{1}{2m}\sum_{ij}\left[A_{ij}-\frac{k_i k_j}{2m}\right]\delta(c_i,c_j)
通俗说明:$A_{ij}$ 是真实桥,$k_i k_j/2m$ 是随机重连时“可能”出现的期望桥数;两者之差越大,说明划分越不像随机。
  1. 邻接矩阵 $A_{ij}$如果岛屿 $i$ 和 $j$ 之间有桥则记为 1,否则为 0。
    Aij∈{0,1}A_{ij}\in\{0,1\}
  2. 度数乘积除以 2m在保持每个人热度不变时,$i$ 和 $j$ 随机碰面的期望边数。
    kikj2m\frac{k_i k_j}{2m}
  3. 社区指示 $\delta(c_i,c_j)$只有当 $i$ 和 $j$ 在同一个社区时才取 1,否则为 0。
    δ(ci,cj)={1,ci=cj0,ci≠cj\delta(c_i,c_j)=\begin{cases}1,&c_i=c_j\\0,&c_i\neq c_j\end{cases}
  4. 对全图求和把所有节点对的真实/期望差距累加,除以 2m 归一化。
    12m∑ij[Aij−kikj2m]δ(ci,cj)\frac{1}{2m}\sum_{ij}\left[A_{ij}-\frac{k_i k_j}{2m}\right]\delta(c_i,c_j)

复杂度剖析

市长看到规划师每拆一座桥就把所有居民问一遍“你最近走哪条路”,忍不住问:这得拆到什么时候?事实上,Girvan-Newman 算法在大型网络上确实很慢,工程落地时必须先算清这笔账。

每轮都要对每个源点做一次 BFS 统计边介数,BFS 遍历全部边,复杂度为 $O(mn)$;而每条边都要被移除一次,最多迭代 $m$ 轮,因此总复杂度为 $O(m^2 n)$。在稠密大图上,这个成本难以承受,这也是该算法的主要瓶颈。

因此实际工程常采用并行化、近似边介数算法,或改用基于模块度优化的 Louvain 等方法。GN 算法更适合中等规模网络,或需要层次化社区结构的小型数据;理解它的复杂度,有助于选择合适的技术路线。

银河城如果桥少还好,一旦桥多如蛛网,这位“人工规划师”就会累垮。所以真实项目里,大城市场景需要更暴力的并行拆桥队或近似公式。

T(n,m)=O(m2n)\displaystyle T(n,m)=O(m^2 n)
通俗说明:边数 $m$ 越多,需要拆桥的次数越多,每拆一次还要重新计算全图路径,耗时按 $m$ 的平方级别增长。
  1. 边数 $m$网络中的桥总数,决定最多拆除多少轮。
    m=∣E∣m=|E|
  2. 每轮 BFS 成本 $O(mn)$从每个节点出发做 BFS,遍历所有节点和边。
    O(mn)O(mn)
  3. 总迭代 $m$ 次每轮拆一条桥,最多拆 $m$ 次。
    O(m)×O(mn)=O(m2n)O(m)\times O(mn)=O(m^2 n)

工程实战

纸上谈兵结束,规划师把银河城地图输入电脑。用邻接表记录小岛和桥,使用现成的 networkx 或手写 BFS,都能完成“拆桥实验”。关键是数据建模要清晰:节点是什么、边是什么、是否带权重,都要先定义好。

工程步骤很直接:建图;循环计算边介数并移除最大边;每轮记录连通分量;用模块度从分裂树中挑出最佳划分。手写实现能帮助理解原理,而生产环境中可以使用 NetworkX 的 girvan_newman 生成器,配合社区发现库计算模块度,自动返回最优社区数。

写代码时还要注意:边介数必须对当前剩余图重新计算;社区划分通过连通分量获得;最终选择模块度最大的层次。下面第 4 节的 Python 演示会完整走一遍这个流程。

电脑生成的社区地图,就是规划师交给市长的成品。有了它,银河城终于知道哪几个岛屿该组成一个“街道办”。

k∗=arg⁡max⁡kQ(Ck)\displaystyle k^*=\arg\max_k Q(C_k)
通俗说明:在所有拆桥层次对应的社区数中,选模块度最大的那一个作为最终社区数量。
  1. 候选划分 $C_k$第 $k$ 个社区数对应的一个划分。
    Ck={C1(k),…,Ck(k)}C_k=\{C_1^{(k)},\ldots,C_k^{(k)}\}
  2. 模块度 $Q(C_k)$对这个划分计算居民满意度。
    Q(Ck)Q(C_k)
  3. 取最大值遍历所有 $k$,找到一个满意度最高的社区数。
    k∗=arg⁡max⁡kQ(Ck)k^*=\arg\max_k Q(C_k)

AI 应用场景

社交网络兴趣群组识别

阶段:特征工程与群体挖掘

解决的问题:传统按关注量或地域划分用户,无法反映真实社群;Girvan-Newman 通过边介数找到互动瓶颈,把紧密交流的用户自动划成一个兴趣圈子。

具体案例:某内容平台对 1 万用户的好友关系网络运行 GN 算法,移除高介数边的“互动桥”后,识别出 20 个兴趣群组,帮助推荐系统做更精准的个性化投放。

生物网络功能模块发现

阶段:模型解释与知识发现

解决的问题:蛋白质相互作用网络规模庞大,人工找功能模块十分费时;GN 算法通过拆“调控桥”识别出相对独立的功能单元。

具体案例:在酵母蛋白质互作网络中应用 GN 算法,研究者成功分离出与细胞周期、代谢通路相关的功能模块,为后续实验提供候选靶点。

金融风控团伙检测

阶段:异常检测与反欺诈

解决的问题:交易网络中欺诈团伙之间往往通过“桥梁账户”关联,直接看局部交易很难发现整体结构;GN 算法移除高介数边后,可暴露可疑的团伙边界。

具体案例:某银行基于转账网络运行 GN 算法,从 10 万账户中识别出多个循环转账的社区,风控团队据此冻结了多个可疑交易团。

Python 演示

python127 行
from collections import deque, defaultdict  def edge_betweenness(adj, edges):    # 边介数: 统计每条边被多少最短路径经过    edge_flow = {tuple(sorted(e)): 0.0 for e in edges}    for s in adj:        parent = {s: []}        sigma = {s: 1}        dist = {s: 0}        queue = deque([s])        order = [s]        while queue:            v = queue.popleft()            for w in adj[v]:                if w not in dist:                    dist[w] = dist[v] + 1                    queue.append(w)                    parent[w] = [v]                    sigma[w] = sigma[v]                    order.append(w)                elif dist[w] == dist[v] + 1:                    parent[w].append(v)                    sigma[w] += sigma[v]        delta = defaultdict(float)        for v in reversed(order):            for p in parent[v]:                edge_key = tuple(sorted((v, p)))                coef = (sigma[p] / sigma[v]) * (1.0 + delta[v])                edge_flow[edge_key] += coef                delta[p] += coef    for key in edge_flow:        edge_flow[key] /= 2.0    return edge_flow  def connected_components(adj):    # 用连通分量表示当前社区    seen = set()    comps = []    for node in adj:        if node in seen:            continue        comp = set()        stack = [node]        seen.add(node)        while stack:            v = stack.pop()            comp.add(v)            for w in adj[v]:                if w not in seen:                    seen.add(w)                    stack.append(w)        comps.append(comp)    return comps  def modularity(edges, communities):    # 基于原始图计算模块度    node_set = set()    for u, v in edges:        node_set.add(u)        node_set.add(v)    nodes = list(node_set)    index = {node: i for i, node in enumerate(nodes)}    n = len(nodes)    A = [[0] * n for _ in range(n)]    deg = [0] * n    for u, v in edges:        i, j = index[u], index[v]        A[i][j] += 1        A[j][i] += 1        deg[i] += 1        deg[j] += 1    m = len(edges)    label = [-1] * n    for cid, comp in enumerate(communities):        for node in comp:            label[index[node]] = cid    Q = 0.0    for i in range(n):        for j in range(n):            if label[i] == label[j]:                Q += A[i][j] - deg[i] * deg[j] / (2.0 * m)    return Q / (2.0 * m)  def girvan_newman_steps(adj, edges, max_communities):    # 复制一份可变图与边表    adj = {k: set(v) for k, v in adj.items()}    edges = [tuple(e) for e in edges]    all_edges = list(edges)    partitions = []    while len(edges) > 0:        flow = edge_betweenness(adj, edges)        e = max(edges, key=lambda x: flow[x])        adj[e[0]].discard(e[1])        adj[e[1]].discard(e[0])        edges.remove(e)        comps = connected_components(adj)        partitions.append([set(c) for c in comps])        if len(comps) >= max_communities:            break    best = None    best_q = -float("inf")    for comps in partitions:        q = modularity(all_edges, comps)        if q > best_q:            best_q = q            best = comps    return best, best_q  if __name__ == "__main__":    # 银河城示例: 左右两个三角岛群由两座桥连接    adj = {        0: {1, 2},        1: {0, 2, 4},        2: {0, 1, 3},        3: {2, 4, 5},        4: {1, 3, 5},        5: {3, 4},    }    edges = [(0, 1), (0, 2), (1, 2), (1, 4), (2, 3), (3, 4), (3, 5), (4, 5)]    best_comms, q = girvan_newman_steps(adj, edges, 2)    print("最佳划分:", best_comms)    print("模块度:", round(q, 4))
目的:手把手实现 Girvan-Newman 核心流程,输出模块度最高的社区划分。
  1. for s in adj:遍历每个源点,作为起点统计到所有其他节点的最短路径。
  2. sigma[w] += sigma[v]当发现从多条前驱到达同一节点的最短路径时,累加路径条数。
  3. coef = (sigma[p] / sigma[v]) * (1.0 + delta[v])按依赖累积公式,统计每条边被使用的流量。
  4. flow = edge_betweenness(adj, edges)在每一轮重新计算当前网络的所有边介数。
  5. e = max(edges, key=lambda x: flow[x])选择边介数最大的桥作为本轮拆除对象。
  6. partitions.append([set(c) for c in comps])记录每轮拆除后的社区划分。
  7. q = modularity(all_edges, comps)用原始图评估当前划分的模块度,找出最佳拆桥层次。
  8. print("最佳划分:", best_comms)输出模块度最高时对应的社区集合。

常见误区

误区以为只需找出原网络上边介数最大的几条边,一次性全删掉就是社区。

正解必须一条一条拆,每拆一条后网络结构都会变化,最短路径与边介数必须重新计算。一次性全删会丢失层次信息,得到错误的社区边界。

为何常见:“高介数边”听起来像一批边界桥,容易被误当成静态清单直接批量删除。

误区认为节点介数最高的节点所连的边就是介数最高的边,只算节点最重要即可。

正解节点介数衡量节点是否处于路径中枢,边介数衡量某条桥是否被大量最短路径经过,二者统计对象不同。Girvan-Newman 要求的是边介数,不能用节点介数代替。

为何常见:两个术语相似,而且都通过最短路径计算,初学者很容易混用。

误区认为 GN 算法要拆到每个节点都成为单独社区,这样最精细。

正解过细划分会让社区失去意义。算法会生成完整分裂树,但最终应使用模块度选择最佳层次,而不是无脑拆到底。

为何常见:教材常展示完整分裂树,让人误以为最底层才是算法答案。

误区以为 GN 能像聚类算法一样高效运行,可以处理百万节点的大图。

正解GN 每移除一条边都要重新计算边介数,最坏复杂度为 $O(m^2 n)$,大图上耗时不可接受。它更适合中等规模网络,或需要层次化社区结构的数据。

为何常见:现代图算法库封装良好,容易让人低估底层算法的真实开销。

误区认为边介数最高的边一定连接两个社区,所以只要删掉它们就能得到正确划分。

正解高介数边可能是社区间的“必经咽喉”,但也可能是细长社区内部的“主干道”。必须结合层次结构和模块度判断,而不是机械删边。

为何常见:看到桥边被反复强调,容易忽略特殊拓扑结构下的反例。

面试题

Q1. Girvan-Newman算法每轮拆除的边是?

单选题 · easy

  • A.连接边数最多的一条边
  • B.边介数数值最大的一条边← 正确答案
  • C.节点度之和最大的一条边
  • D.随机挑选图中任意一条边

答案:B。算法模仿“拆桥”,优先拆除当前边介数最大的桥,而不是连边数或节点度最大的边。边介数衡量的是必经路径流量,所以 B 正确。

Q2. 边介数衡量的是什么?

单选题 · easy

  • A.所有最短路径中经过该边的数量← 正确答案
  • B.网络中所有边的长度的总和
  • C.该边连接的两个节点的度数
  • D.该边在最小生成树中出现的次数

答案:A。边介数关注“必经流量”,即所有节点对的最短路径中,有多少条穿过这条边。它不关心边的物理长度,也不关心节点度数。

Q3. 以下哪些是社区发现的典型应用场景?

多选题 · easy

  • A.社交圈子挖掘← 正确答案
  • B.蛋白质功能预测← 正确答案
  • C.搜索引擎排序
  • D.银行客群划分← 正确答案

答案:ABD。社区发现可用于社交、生物和金融网络;搜索引擎排序主要基于链接重要性或用户行为,不是社区发现的典型目标。

Q4. 模块度 Q 越接近 1,说明什么?

单选题 · mid

  • A.社区内部连接越稀疏
  • B.社区划分质量越高← 正确答案
  • C.网络节点数量越多
  • D.拆掉的桥数量越少

答案:B。Q 比较真实社区内部连边与随机期望的差距,差距越大说明“社区感”越强。Q 越接近 1,划分质量越高,而不是内部稀疏或节点多。

Q5. Girvan-Newman 每轮移除边后,下一步必须做什么?

单选题 · mid

  • A.重新计算所有边介数← 正确答案
  • B.随机加入一条新边
  • C.合并相似的两个节点
  • D.只更新节点度数即可

答案:A。拆桥后最短路径会改变,原先计算的边介数失效,必须重新统计。这就像拆桥后要重新统计居民出行路线。

Q6. 分裂树主要用于展示什么?

单选题 · mid

  • A.节点的层次化聚类关系← 正确答案
  • B.边介数的具体数值
  • C.模块度的变化曲线
  • D.网络直径的变化过程

答案:A。分裂树记录每一步拆桥后的连通分量,体现节点从整图逐渐分裂成社区的层次关系,是理解 GN 层次输出的核心工具。

Q7. 哪些因素导致 Girvan-Newman 算法复杂度较高?

多选题 · mid

  • A.每轮需要重新计算边介数← 正确答案
  • B.需要遍历节点对的最短路径← 正确答案
  • C.每轮只能移除一条边← 正确答案
  • D.需要重复进行凝聚聚类

答案:ABC。A 和 B 来自边介数计算本身,C 导致迭代轮数多;D 属于凝聚式层次聚类,与 GN 的分裂式思路无关。

Q8. Brandes 方法计算边介数的关键思想是什么?

单选题 · hard

  • A.按 BFS 层次从远到近累积依赖← 正确答案
  • B.按 DFS 深度优先搜索全部路径
  • C.随机游走模拟大量旅客通行
  • D.仅统计叶子节点间的最短路径

答案:A。Brandes 方法利用 BFS 层次结构,从距源点最远的节点开始反向累积路径依赖,从而避免枚举所有最短路径,显著提升效率。

Q9. 模块度公式中涉及哪些量?

多选题 · hard

  • A.实际邻接矩阵中的连边信息← 正确答案
  • B.随机网络下的期望连边数← 正确答案
  • C.社区归属指示函数← 正确答案
  • D.网络中的全部最短路径总数

答案:ABC。模块度包含 $A_{ij}$、$k_i k_j/2m$ 和 $\delta(c_i,c_j)$,分别对应 A、B、C。最短路径总数属于边介数,不属于模块度。

Q10. 含 m 条边、n 个节点的网络上,Girvan-Newman 算法最坏时间复杂度约为?

单选题 · extreme

  • A.O(m^2 n)← 正确答案
  • B.O(mn^2)
  • C.O(n^3)
  • D.O(m log n)

答案:A。每轮边介数计算需要 BFS 遍历图,成本为 O(mn),最多拆 m 条边,因此总体为 O(m^2 n)。这也是该算法在大规模网络上的主要瓶颈。