题目描述
思路解析
一句话答案:LeetCode 2685 统计完全连通分量的数量:DFS 逐块数出顶点数 x 和度数和 y,判 x 乘 x 减 1 是否等于 y,成立就是完全分量,时间 O(n+m)。
无向图里,哪几块顶点算「完全」
给整数 n 和无向边列表 edges,顶点从 0 编到 n 减 1,返回完全分量的个数。连通分量,也就是图里能互相连得到的一撮点、且不和外面的点共边;一块分量若每一对顶点都直接有边,就叫完全分量。题面第一组 n=6、edges=[[0,1],[0,2],[1,2],[3,4]]:0、1、2 连成三角形、3 和 4 连一条、5 是孤点,三块都完全,答案 3。
一块块去数边、判边数够不够,卡在哪
一块 x 个顶点要两两都连上得有 x 乘以 x 减 1 再除以 2 条边。照定义硬做,就得对每块单独攒一个边集合、数够几条再和目标数比。可每条边 [a,b] 遍历时会从 a、b 各看一次,稍不留神就把一条边数成两条,维护边集合还白占存储。
把边数换成度数和,一趟就判出来
遍历一块分量时,把每个顶点的度数,也就是它连着几个邻居,全加起来记成 y。每条边连着两个端点、在两端度数里各被算一次,所以 y 恰好是边数的 2 倍。原判据「边数等于 x 乘 x 减 1 除以 2」两边同乘 2,就变成 y 等于 x 乘以 x 减 1。这样遍历时顺带把 y 累出来,不用单独数边、也不怕重复。
外层扫起点,DFS 一趟带回 x 和 y
先按 edges 建无向邻接表,每条边两个方向都记上,再开 vis 数组记谁访问过。外层 i 从 0 扫到 n 减 1,遇到没访问的顶点就以它为起点做一次 DFS,顺着邻居把整块分量一路探遍。DFS 每到一个点把 x 加 1、度数加进 y,走完返回这块 x 和 y,外层套一次 x 乘 x 减 1 是否等于 y,成立答案加 1。
拿题面 n=6 那组数据推到底
外层从 0 起,第一块走遍 0、1、2,x 数到 3,三点度数都是 2、y 加成 6;3 乘 2 等于 6 正好等于 y,完全,答案记 1。扫到 3 进第二块 3、4,x 是 2、度数各 1、y 是 2;2 乘 1 等于 2 等于 y,也完全,答案到 2。最后 5 一条边都没有单独成块,x 是 1、y 是 0;1 乘 0 等于 0 等于 y,还是完全,答案到 3,和第一组要的 3 对上。若按第二组补上 [3,5],3、4、5 并成一块 x 是 3、度数只有 1、1、2、y 才 4,6 不等于 4,不算,答案只剩 1。
复杂度,以及几处一算就错的地方
建表扫一遍所有边、DFS 每点访问一次每条边看两次,时间 O(n+m);邻接表存双向边加上 vis 和递归栈,空间 O(n+m)。几处容易算错:孤立单点别当成不完全漏掉,它 x 是 1、x 乘 x 减 1 等于 0 正好等于 y,也是完全分量必须计入;邻接表只加一个方向会让度数算少、分量走不全,无向图务必两向都加;判据是 y 等于 x 乘 x 减 1、别写成等于它的一半,因为 y 已是 2 倍边数;n 很小,int 就够不必换 long。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记牢这一句:一块连通分量,顶点数是 x、度数和是 y,只要 x 乘以 x 减 1 等于 y,它就是完全分量。用度数和来判,省得单独去数边、也不怕重复。下面从顶点 0 开始,一块一块地走。
先看整张图 · 三块相连的顶点:这是我们要处理的图。一眼看去,顶点自然分成三块:左边 0、1、2 三个点两两连着,像个三角形;中间 3、4、5,3 分别连到 4 和 5,可 4 和 5 之间是断开的;右边 6 是个光杆,一条边都没有。这三块就是三个连通分量,接下来逐块检验它们完不完全。
什么叫完全 · 每对顶点都要有边:先把完全的判据说清。一块有 x 个顶点,要让它们两两之间都直接有边,一共需要 x 乘以 x 减 1 再除以 2 条边。1 个点不需要边,天生完全;2 个点要 1 条边;3 个点要 3 条边,也就是凑成一个三角形。等会儿每走完一块,就拿它实际的边数和这个应有的边数比一比。
巧劲 · 用度数和代替数边:这里有个省事的巧劲。与其小心翼翼地数边,不如把块里每个顶点的度数加起来,记成 y。因为每条边连着两个端点,会在两个端点的度数里各被算一次,所以 y 刚好是边数的 2 倍。这么一来,完全的条件从「边数等于 x 乘 x 减 1 除以 2」直接变成「y 等于 x 乘 x 减 1」,遍历时顺手就把 y 累出来了。
外层扫描 · 找未访问的起点:整体框架是这样:外层用 i 从 0 扫到 6,一旦碰到一个还没被访问过的顶点,就以它为起点发起一次深度优先遍历,把它所在的一整块分量走遍,同时把这块的 x 和 y 累出来。走过的点随手标记已访问,外层就不会重复进同一块。下面开始扫。
发现分量一 · 起点 0:外层从 0 开始,0 还没访问过,就以它为起点发起 DFS。开一个计数:顶点数 x 先记 1,度数和 y 先把 0 的度数加进去。0 连着 1 和 2 两条边,度数是 2,所以 y 现在是 2。0 就是这块分量的第一个顶点。
在 0 上 · 看邻居 1、2:站在 0,把它标成正在处理。看它的邻接表,邻居是 1 和 2,都还没访问。深度优先的规矩是先认准一个邻居钻到底,再回头处理另一个。按建图的顺序,先走去 1。两条从 0 出发的边先点亮,表示待会儿都要走。
沿边 0-1 到 1:顺着边 0-1 走到 1,把它收进这块分量。x 从 1 加到 2。1 也连着两条边,连到 0 和 2,度数是 2,把它加进 y,y 从 2 变成 4。1 这个点先标成刚够到的蓝色,马上轮到处理它。
在 1 上 · 唯一没走的邻居是 2:轮到处理 1。它的邻居是 0 和 2,0 刚才已经访问过了,跳过,只剩 2 还没走。那就沿着边 1-2 继续往下钻。深度优先就是这样一条路走到底,把能连的都串起来。
沿边 1-2 到 2:走到 2,收进分量,x 加到 3。2 连着 0 和 1 两条边,度数是 2,加进 y,y 从 4 变成 6。到这儿这块分量的三个顶点 0、1、2 都收齐了,x 是 3,y 是 6。
在 2 上 · 0 与 2 之间也有边:处理 2 的时候,它的邻居 0 和 1 都访问过了,这块就到头了。注意看,除了走过的 0-1、1-2,0 和 2 之间同样有一条边。也就是说 0、1、2 这三对顶点,每一对都直接连着。这一点度数和已经替我们记下了:y 等于 6。
分量一结算 · 完全分量,答案 +1:这块分量走完,x 是 3、y 是 6。套判据:x 乘以 x 减 1 等于 3 乘 2 等于 6,正好等于 y。等式成立,0、1、2 是一个完全分量。把三个顶点整块涂绿,完全分量数从 0 加到 1。
发现分量二 · 起点 3:外层继续往后扫,1、2 都访问过了,扫到 3,它还没访问,又发起一次 DFS。计数重新开张:x 记 1。3 连着 4 和 5 两条边,度数是 2,y 先记 2。开始走这块新的分量。
在 3 上 · 看邻居 4、5:站在 3,看它的邻居,是 4 和 5,都没访问。按顺序先钻去 4。两条从 3 出发的边先点亮。你先记着一个悬念:3 分别连着 4 和 5,可 4 跟 5 彼此之间有没有边,得走过去才知道。
沿边 3-4 到 4:顺着边 3-4 到 4,收进分量,x 加到 2。可这次不一样:4 只连着 3 这一条边,度数是 1,加进 y 只让 y 从 2 变成 3。度数和涨得慢,已经在暗示这块的边不太够了。
在 4 上 · 4 只连着 3:处理 4,它的邻居只有一个 3,还是已经访问过的。这条路到头了,退回 3。你看,4 这个点是个叶子,除了拴在 3 上,和别人没有任何联系。回到 3 去走它另一个还没走的邻居 5。
沿边 3-5 到 5:回到 3,沿边 3-5 走到 5,收进分量,x 加到 3。5 也一样,只连着 3 这一条边,度数是 1,y 从 3 变成 4。三个顶点 3、4、5 都收齐了,可 x 是 3、y 只有 4。
关键 · 4 与 5 之间没有边:把这块看清楚:3 像个中心,分别拉着 4 和 5,可是 4 和 5 之间空着,没有边。3 个顶点要想两两相连得有 3 条边,凑成三角形,这里只有 3-4、3-5 两条。缺了 4-5 那条,这块就不是完全分量。
分量二结算 · 不完全,不计数:算一下:x 乘以 x 减 1 是 3 乘 2 等于 6,可 y 只有 4,等式不成立。所以 3、4、5 这块不是完全分量,不计数。把它们恢复成常态的灰色,完全分量数还是 1,一个没加。度数和差在哪儿一目了然:少一条边,y 就少 2。
发现分量三 · 起点 6(孤立点):外层扫到最后一个点 6,它还没访问,发起 DFS。6 是个孤立点,一条边都没有。x 记 1,它的度数是 0,y 就是 0。这块分量只有它自己一个顶点。
在 6 上 · 没有任何邻居:处理 6,它的邻接表是空的,没有邻居可走,这块分量就它一个点,直接结束。x 是 1,y 是 0。别小看这种单点分量,它其实是完全分量里最特殊的一种。
分量三结算 · 单点也是完全,答案 +1:套判据:x 乘以 x 减 1 是 1 乘 0 等于 0,y 也是 0,等式成立。所以单个孤立点 6 也是一个完全分量,因为它里面根本没有「两个顶点」需要相连,判据自动满足。把 6 涂绿,完全分量数从 1 加到 2。
回放 · 两个完全分量,答案 2:三块分量全部查完。0、1、2 三角形每对都连,完全,绿的;6 孤立点没有任何一对要连,也算完全,绿的;3、4、5 缺了 4-5 那条边,不完全,灰的。数一数绿的分量,一共 2 个,这就是答案。判定自始至终只有一句话:x 乘以 x 减 1 等不等于度数和 y。
边界想清:单点算 1 个完全分量、两个孤立点算 2 个、三点成链缺一条边则不完全记 0。
面试重点:逐分量数 x 和度数和 y、判 x 乘 x 减 1 等于 y、可换并查集、复杂度 O(n 加 m)。
参考代码
from __future__ import annotationsfrom typing import *from collections import *from functools import *from itertools import *from math import *from heapq import *from bisect import *class Solution: def countCompleteComponents(self, n: int, edges: List[List[int]]) -> int: def dfs(i: int) -> (int, int): vis[i] = True x, y = 1, len(g[i]) for j in g[i]: if not vis[j]: a, b = dfs(j) x += a y += b return x, y g = defaultdict(list) for a, b in edges: g[a].append(b) g[b].append(a) vis = [False] * n ans = 0 for i in range(n): if not vis[i]: a, b = dfs(i) ans += a * (a - 1) == b return ans复杂度
- 时间:O(n + m),n 个顶点 m 条边。建邻接表扫一遍所有边是 O(m);DFS 阶段每个顶点恰好被访问一次、每条边在两个端点各被看一次,合起来是 O(n + m)。整体随顶点数加边数线性增长
- 空间:O(n + m),按峰值算。邻接表存了每条边的两个方向,占 O(n + m);vis 数组 O(n);递归栈最坏在一条链状分量铺满时到 O(n)。峰值是 O(n + m)
易错点
面试追问把动画讲成自己的话
追问这题的核心判定是什么?
追问为什么用度数和而不去数边?
追问除了 DFS 还能怎么做,复杂度多少?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
单词接龙 II
LeetCode 126 · 困难 · 沿着 图论套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题