并查集:连通性判定
本节目标
- 理解并查集(DSU)的"找根"与"合并"
- 掌握路径压缩与按秩合并两个优化
- 能手写"省份数量 / 朋友圈"类题
并查集:管理"若干不相交集合",支持"查某元素属于哪伙(find)"和"把两伙合并(union)"。常用于连通分量、动态连通性。
// 运行环境:Node.js 14+
// 保存为 dsa-l14.js,执行:node dsa-l14.js
class DSU {
constructor(n) {
this.parent = Array.from({ length: n }, (_, i) => i) // 初始各自为营
this.rank = new Array(n).fill(0) // 树高估计
}
find(x) { // 找根 + 路径压缩
while (this.parent[x] !== x) {
this.parent[x] = this.parent[this.parent[x]] // 路径压缩:跳到爷节点
x = this.parent[x]
}
return x
}
union(a, b) { // 按秩合并
const ra = this.find(a), rb = this.find(b)
if (ra === rb) return false // 已同伙
if (this.rank[ra] < this.rank[rb]) this.parent[ra] = rb
else if (this.rank[ra] > this.rank[rb]) this.parent[rb] = ra
else { this.parent[rb] = ra; this.rank[ra]++ }
return true
}
}
// 省份数量:n 个城市,isConnected[i][j]=1 表示相连
function provinces(isConnected) {
const n = isConnected.length
const dsu = new DSU(n)
for (let i = 0; i < n; i++)
for (let j = i + 1; j < n; j++)
if (isConnected[i][j]) dsu.union(i, j)
let count = 0
for (let i = 0; i < n; i++) if (dsu.find(i) === i) count++
return count
}
console.log(provinces([[1,1,0],[1,1,0],[0,0,1]])) // 2(两个连通块)名词解释
- 并查集 / 不相交集合(DSU/UF):维护若干"互不相交集合",支持
find(查所属集合根)与union(合并两集合)。适合动态连通性问题。 - 路径压缩(Path Compression):
find时把沿途节点直接挂到根上,拉平树高,使后续find接近 O(1)。 - 按秩合并(Union by Rank):合并时把"矮树"挂到"高树"下,避免树退化成链。两优化叠加,
find/union近乎 O(1)(反阿克曼函数)。
课后练习
- 并查集为什么
find要路径压缩?- 答案:不压缩时树可能退化成链,
find退化为 O(n);压缩后把访问过的节点直接连到根,均摊复杂度降到几乎常数(反阿克曼)。
- 答案:不压缩时树可能退化成链,
provinces里if (dsu.find(i) === i) count++在数什么?- 答案:数"根节点"的个数。每个连通块有且只有一个根(parent[x]===x),根的个数即连通块(省份)数。
总结
并查集是那种"一听名字觉得冷门、一用就离不开"的结构。它解决的其实是世界上最朴素的问题:哪些东西连在一起?无论是朋友圈分组、网络连接、还是岛屿连通,本质都是"连通分量计数"。它的精妙在于两个小优化——路径压缩和按秩合并——单独看都平淡无奇,组合起来却让 find/union 的复杂度降到近乎 O(1),这种"简单操作叠加出惊人效果"的设计哲学非常值得品味。我建议把 DSU 当成一个"即插即用"的工具类记在脑子里:遇到"动态加边、随时问连通性"的题,别想着每次 BFS/DFS 重算,直接掏出并查集,一步到位。