并查集:连通性判定

本节目标

并查集:管理"若干不相交集合",支持"查某元素属于哪伙(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(两个连通块)

名词解释

课后练习

  1. 并查集为什么 find 要路径压缩?
    • 答案:不压缩时树可能退化成链,find 退化为 O(n);压缩后把访问过的节点直接连到根,均摊复杂度降到几乎常数(反阿克曼)。
  2. provinces 里 if (dsu.find(i) === i) count++ 在数什么?
    • 答案:数"根节点"的个数。每个连通块有且只有一个根(parent[x]===x),根的个数即连通块(省份)数。

总结

并查集是那种"一听名字觉得冷门、一用就离不开"的结构。它解决的其实是世界上最朴素的问题:哪些东西连在一起?无论是朋友圈分组、网络连接、还是岛屿连通,本质都是"连通分量计数"。它的精妙在于两个小优化——路径压缩和按秩合并——单独看都平淡无奇,组合起来却让 find/union 的复杂度降到近乎 O(1),这种"简单操作叠加出惊人效果"的设计哲学非常值得品味。我建议把 DSU 当成一个"即插即用"的工具类记在脑子里:遇到"动态加边、随时问连通性"的题,别想着每次 BFS/DFS 重算,直接掏出并查集,一步到位。