并查集实现"朋友圈"与动态连通性
本节目标
- 用并查集实现"朋友圈分组"统计
- 支持"动态加好友"后实时查询连通性
- 体会 DSU 从"算法题"到"工程组件"的跨越
场景:社交 App 里,A 和 B 是好友、B 和 C 是好友,则 A/B/C 同属一个朋友圈。随着好友关系不断新增,要能随时回答"某两人是否同圈""当前共有几个圈"。这正是并查集的用武之地。
// 运行环境:Node.js 14+
// 保存为 dsa-l22.js,执行:node dsa-l22.js
class FriendCircle {
constructor(n) {
this.parent = Array.from({ length: n }, (_, i) => i)
this.rank = new Array(n).fill(0)
this.circles = n // 初始每人一个圈
}
find(x) {
while (this.parent[x] !== x) { this.parent[x] = this.parent[this.parent[x]]; x = this.parent[x] }
return x
}
addFriend(a, b) { // 动态加好友
const ra = this.find(a), rb = this.find(b)
if (ra === rb) return // 本就同圈
if (this.rank[ra] < this.rank[rb]) this.parent[ra] = rb
else { this.parent[rb] = ra; if (this.rank[ra] === this.rank[rb]) this.rank[ra]++ }
this.circles-- // 合并→圈数减一
}
sameCircle(a, b) { return this.find(a) === this.find(b) }
countCircles() { return this.circles }
}
// 调用示例
const fc = new FriendCircle(5) // 用户 0..4
fc.addFriend(0, 1)
fc.addFriend(1, 2)
fc.addFriend(3, 4)
console.log(fc.sameCircle(0, 2)) // true(0-1-2 同圈)
console.log(fc.sameCircle(0, 3)) // false
console.log(fc.countCircles()) // 2({0,1,2} 与 {3,4})工程映射:把"加好友"换成"网络连接""文件去重""图片连通区域",底层全是同一套 DSU。它比"每次 BFS 重算连通"高效得多——增边 O(α(n)),查询 O(α(n))。
名词解释
- 动态连通性(Dynamic Connectivity):图不断加边(或删边)的过程中,实时回答"两点是否连通"。并查集是加边场景下的最优解之一。
- 朋友圈 / 连通分量(Connected Component):图中"互相可达"的顶点集合。整个图的连通分量个数 = 并查集里根节点的个数。
- 几乎常数时间 α(n):反阿克曼函数,增长极慢,n 取宇宙原子数也小于 5,实际可视为常数。
课后练习
- 为什么"加好友"用并查集比每加一个就 BFS 重算好?
- 答案:BFS 每次 O(V+E),频繁加边会累积成 O(Q·(V+E));并查集每次合并/查询近乎 O(1),Q 次操作总代价 O(Q·α(n)),量级碾压。
addFriend里this.circles--为什么只在"原本不同圈"时减一?- 答案:同圈合并不改变圈数(本来就是一伙);只有把两个不同圈并在一起,圈的总数才减少 1。
总结
这一节是并查集从"算法练习题"走向"工程组件"的关键一跃。它告诉我们一个朴素真理:很多看似不同的业务问题(朋友圈、网络连通、图像分割、数据库去重),抽象到最后都是同一个数学模型——动态连通性。并查集之所以是这个问题的标准答案,是因为它在"不断加边、随时查询"的模式下,把每次操作压到几乎常数时间,这是任何"重新遍历"方案都做不到的。更妙的是,路径压缩 + 按秩合并这两个小技巧,让它的复杂度低到反阿克曼函数(实际等同常数)。我的体会是:当你学会把业务语言翻译成"并查集操作"(加边=union、查询=sameSet、计数=根数),你就拥有了解决一类分布式/图连通问题的通用钥匙,这种"抽象能力"比背代码值钱得多。