跳到主要内容

连通性、并查集与二分图

本节目标

从零建立并查集的 parent 森林、路径压缩与按大小合并,并用染色判断二分图。

无向图中的“是否已经连通”经常伴随不断加入边:朋友关系、城市道路和网络组件都在问两个点是否属于同一组。并查集(Disjoint Set Union, DSU)专门维护一批互不重叠的集合;二分图则是另一种连通分量问题,它要求每条边两端能被染成不同颜色。

识别信号

  • 无向边持续加入,需要快速询问两点是否已在同一连通块;
  • 加一条边后,若两端本已连通,这条边就是环或冗余边;
  • 邻接矩阵描述“直接或间接相连”的城市、账号或节点组;
  • 要把图分成两个阵营,且每条边必须跨阵营;
  • 图可能不连通,不能只从顶点 0 出发。

问题模型与核心不变量

从 parent 森林开始

为每个元素建立一个 parent[i]。初始化 parent[i] = i:每个元素独自是一个集合,也是一棵只有根的树。多个集合并排放在一起,就是一片 parent 森林。树根满足 parent[root] == root,并且一个集合恰好对应一棵树。

find(x) 沿着 parent 指针走到根,返回该集合唯一的代表元。于是 find(a) == find(b) 当且仅当 a、b 已连通。路径压缩在回程时把沿途节点直接改指向根:下一次查找不用再逐层爬树。

unite(a, b) 先找两个根。根相同,合并失败并返回 false,因为它们本就在同一集合;根不同,把一棵根树接到另一棵根树上,合并成功并返回 true。维护 size[root],总是把小树接到大树,避免树长得过高。路径压缩与按大小合并一起使用后,m 次操作的均摊时间是 O(m α(n)),单次可视为近似常数;α(n) 是增长极慢的反 Ackermann 函数。

并查集只擅长加边、合并和连通查询。它不维护删边后的分裂:删除一条树边可能把一个集合拆成两个,而 parent 森林没有足够信息恢复这个分割。含删边的动态连通性应改用离线算法、可回滚并查集或更专门的数据结构。

二分图不需要 DSU。给每个点保存 color-1 未染色、01 是两组。每个未染色分量都从颜色 0 开始 BFS;访问边 u—v 时,未染色的 v 必须染成 1 - color[u],已染色且与 u 同色则立即矛盾。这个规则遍历全部分量后仍无矛盾,图才是二分图。

通用模板

DSU 初始化:parent[i] = i, size[i] = 1
find(x):若 parent[x] != x,递归/迭代压缩到根;返回根
unite(a, b):
ra = find(a), rb = find(b)
若 ra == rb:返回 false
按 size 令小根挂到大根,更新 size
返回 true

二分图 BFS:
color 全为 -1
for 每个顶点 start:
若已染色则跳过
color[start] = 0,start 入队
出队 u;检查每个邻居 v
未染色则染相反色并入队;同色则返回 false
return true

模板变体

  • 连通分量计数:从 n 开始,每次 unite 成功就减一;最后是集合数。
  • 第一条成环边:顺序扫描边,第一次 unite 失败时当前边就是冗余边。
  • 稠密邻接矩阵:只扫描上三角 i < j,避免对称边重复合并。
  • 二分图 DFS:递归传播颜色也正确;BFS 更容易显式处理每个独立分量。
  • 带敌对关系的分组:可把一个点及其“相反组”编码成两个代表元,再用 DSU;基本二分图题先用直接染色更清晰。

母题序列

必学顺序练习:

  1. 省份数量必学):把邻接矩阵中的城市边合并,计数剩余 parent 树根。
  2. 冗余连接必学):借助 unite 的失败返回值定位第一条闭环边。
  3. 判断二分图必学):在每个连通分量内传播相反颜色,寻找同色边。

常见误区

  • find 只返回父亲而非根,间接连通的节点会被误判为不同集合。
  • 合并前不先找根,可能把内部节点接出错误结构。
  • 同根时仍减少连通块数量;重复合并并没有产生新连接。
  • 不做路径压缩和按大小合并,在长链上让查询退化。
  • 用并查集处理删边并期待集合自动拆开;它不支持这个操作。
  • 二分图只染第一个分量,漏掉后面分量里的奇环。
  • 把孤立点当异常;孤立点单独染任意一种颜色都合法。

迁移方向

只加边的“属于同一组吗”优先考虑并查集;需要一条实际路径、最短步数或删边后连通性时,改用图搜索或专门的动态结构。出现“相邻必须不同类”时,先试二染色;若出现奇环,同一套约束就不可满足。后续最小生成树会继续使用 DSU 的 unite,但目标会从“是否连通”变成“以最小代价连接”。