省份数量:相连就并到一块
省份是连通块。矩阵里的 1 是边:并查集把两端并起来,或 DFS 从一座城走完所有能走到的城,块数就是答案。
n 个城市各自成组。扫描邻接矩阵,对 isConnected[i][j]=1(i≠j)的城市 union。结束时 parent 中「根是自己」的节点数量即连通分量数 = 省份数。时间 O(n² α(n)),空间 O(n)。
n 座城市,n×n 矩阵 isConnected[i][j]=1 表示 i 和 j 直接相连,对角线一定是 1。直接或间接连得上的城市同属一个省,求省的个数。
主例 [[1,1,0],[1,1,0],[0,0,1]]:城 0 和城 1 互相连,城 2 只连自己。两个连通块,答案 2。演示从 parent=[0,1,2]、count=3 走到合并 0-1 之后的 count=2。
把矩阵当图,点是城、1 是无向边。本文要回答:为什么数的是连通块而不是边数,以及主例里那一次 union 怎样把 3 个根收成 2 个。
缺口是「谁和谁已经是一省」
第一直觉数矩阵里有多少个 1。主例上三角有一个 1,加上三个对角线,数字对不上省份。边数不是块数:三角形三座城全相连,边有 3,省只有 1。缺的是传递闭包——0 连 1、1 连 2,则 0 和 2 同省,即使矩阵 [0][2] 仍是 0。
并查集直接维护「谁已经和谁一伙」。一开始 n 座城各自为根,count=n。扫到一条边 i—j,先找两端的根;根不同就挂到一起,count 减一;根相同说明早就在一省,边是多余的,count 不动。扫完之后,还在的 count 就是省份。
矩阵对称,i—j 和 j—i 是同一条边,只扫上三角 j>i,对角线是自己连自己,不用 union。find 带路径压缩:顺着 parent 走到根,路上的点都改挂到根上,后面的查找是平的。
同一张图也可以 DFS 或 BFS 数连通块。从每个未访问的城出发走一遍,沿矩阵行把所有 isConnected[u][v]=1 的邻居染上,走完一块省份加一。主例:从 0 出发染到 1,第一省;2 还没染过,第二省。答案同样是 2。并查集适合「边是成对给出、最后问几伙」;DFS 适合「从点出发把能走到的一次走完」。
用手走主例。n=3,parent=[0,1,2],count=3,演示第一帧。扫到 [0][1]=1,find(0)=0、find(1)=1,根不同,parent[1]=0,count 变成 2,演示第二帧。[0][2]=0、[1][2]=0,没有新边。城 2 的根仍是自己。最后一帧省份数 2。
全连成一块时,count 会从 n 减到 1;完全没有边时,count 停在 n。不要把对角线的 1 当成「自己是一个额外的省」——它只是「城存在」,初始化已经数过了。
连通分量一次成功的 union 少一个根,也少一个省。DFS 每开始一次新的遍历也加一个省。两种计数看的是同一批块。主例只有 0-1 这一次合并,2 保持孤立。
Go:并查集计数
func findCircleNum(isConnected [][]int) int {n := len(isConnected)parent := make([]int, n)for i := range parent { parent[i] = i }var find func(int) intfind = func(x int) int {if parent[x] != x { parent[x] = find(parent[x]) }return parent[x]}count := nfor i := 0; i < n; i++ {for j := i + 1; j < n; j++ {if isConnected[i][j] == 1 {a, b := find(i), find(j)if a != b { parent[a] = b; count-- }}}}return count}
1n 座城先各自为根。count 从 n 起,不从 0 起再数根。
2find 路径压缩。比较的是根,不是 parent 槽里的旧值。
3只扫 j>i。主例只会看到 [0][1]=1 这一条有效边。
4根不同才挂边并 count--。已经同省的 1 不要减第二次。
5返回 count。主例停在 2,对应 parent 里两个根 0 和 2。
总结
省份是连通块:并查集合并每条边,或 DFS 染每一块。主例答案 2。
- 边数不是省数。主例只有一条有效边,但要先有 3 个根再减 1。
- 矩阵对称,扫上三角;对角线的 1 不是新省。
- DFS 从每个未访问点出发走完一块,计数与并查集相同。