并查集:连通性问题
1. 为什么需要并查集
假设你刷朋友圈,看到 A 点赞了 B,B 评论了 C,C 转发了 D。直觉告诉你——A、B、C、D 在一个圈子里。判断"两个人是否在同一个圈子",就是连通性问题。
并查集(Union-Find / Disjoint Set Union)专门解决这类问题,支持两个操作:
- Union(x, y):把 x 和 y 所在的集合合并
- Find(x):找出 x 所属的集合的代表元素
2. Quick-Find:暴力连通
最朴素的想法:给每个元素一个"组号",同组元素编号相同。
#include <vector>
#include <iostream>
class QuickFind {
std::vector<int> id; // 组号
public:
QuickFind(int n) : id(n) {
for (int i = 0; i < n; ++i)
id[i] = i; // 初始化:每个人自成一组
}
bool connected(int p, int q) {
return id[p] == id[q]; // O(1) 判断连通
}
void unite(int p, int q) { // union 是关键字,用 unite
int pid = id[p];
int qid = id[q];
if (pid == qid) return;
for (int i = 0; i < id.size(); ++i)
if (id[i] == pid) // 遍历所有元素,改组号
id[i] = qid; // O(n)
}
};
问题:union 操作要遍历整个数组,O(n)。n 一大的数据场景直接崩溃。
3. Quick-Union:树形结构
把"组"换成"树"——每个元素只存自己的父节点,根节点代表整个集合。
#include <vector>
class QuickUnion {
std::vector<int> parent;
public:
QuickUnion(int n) : parent(n) {
for (int i = 0; i < n; ++i)
parent[i] = i;
}
int find(int x) {
while (parent[x] != x)
x = parent[x]; // 沿着父指针向上找根
return x;
}
bool connected(int p, int q) {
return find(p) == find(q);
}
void unite(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ) return;
parent[rootP] = rootQ; // 一棵树挂到另一棵树根
}
};
问题:树可能退化成链表。1→2→3→4→5,find(5) 要走 5 步,union 也是 O(n)。
Quick-Union + 路径压缩
find(5) 走 4 步找到根,路径压缩把沿途节点直接指向根:
压缩后结构:
所有节点直接指向根,下次 find 就是 O(1)。
4. 按秩合并
光压缩还不够——如果 union 时总是把大树挂到小树上,树还是会变高。按秩合并(Union by Rank):记录每棵树的高度(秩),把矮树挂到高树下。
#include <vector>
class UnionFind {
std::vector<int> parent;
std::vector<int> rank; // 树的高度上界
public:
UnionFind(int n) : parent(n), rank(n, 0) {
for (int i = 0; i < n; ++i)
parent[i] = i;
}
int find(int x) {
if (parent[x] != x)
parent[x] = find(parent[x]); // 路径压缩
return parent[x];
}
void unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return;
if (rank[rx] < rank[ry]) {
parent[rx] = ry;
} else if (rank[rx] > rank[ry]) {
parent[ry] = rx;
} else {
parent[ry] = rx; // 同高度,选一个做根
++rank[rx]; // 高度 +1
}
}
bool connected(int x, int y) {
return find(x) == find(y);
}
};
5. 复杂度
| 方法 | union | find | 最坏 |
|---|---|---|---|
| Quick-Find | O(n) | O(1) | O(n²) |
| Quick-Union | O(n) | O(n) | O(n²) |
| 路径压缩 | 几乎 O(1) | 几乎 O(1) | O(log n) |
| 按秩合并 | O(log n) | O(log n) | O(log n) |
| 路径压缩 + 按秩合并 | α(n) | α(n) | α(n) |
α(n) 是反阿克曼函数。对于 n ≤ 10⁸⁰,α(n) ≤ 4。所以实际复杂度是常数。
6. 基础模板(面试通用)
#include <vector>
class UnionFind {
std::vector<int> parent;
std::vector<int> rank;
int count; // 连通分量数
public:
UnionFind(int n) : parent(n), rank(n, 0), count(n) {
for (int i = 0; i < n; ++i)
parent[i] = i;
}
int find(int x) {
if (parent[x] != x)
parent[x] = find(parent[x]);
return parent[x];
}
void unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return;
if (rank[rx] < rank[ry]) {
parent[rx] = ry;
} else if (rank[rx] > rank[ry]) {
parent[ry] = rx;
} else {
parent[ry] = rx;
++rank[rx];
}
--count;
}
int getCount() const { return count; }
};
7. 面试题
7.1 岛屿数量
题目:'1' 是陆地,'0' 是水,求岛屿数。
思路:每个 '1' 是一个节点,相邻的 '1' 合并。最后统计有多少个集合。
#include <vector>
class UnionFind {
std::vector<int> parent, rank;
int count;
public:
UnionFind(int n) : parent(n), rank(n, 0), count(n) {
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return;
if (rank[rx] < rank[ry]) parent[rx] = ry;
else if (rank[rx] > rank[ry]) parent[ry] = rx;
else { parent[ry] = rx; ++rank[rx]; }
--count;
}
int getCount() const { return count; }
};
class Solution {
public:
int numIslands(std::vector<std::vector<char>>& grid) {
if (grid.empty() || grid[0].empty()) return 0;
int m = grid.size(), n = grid[0].size();
UnionFind uf(m * n);
int water = 0;
auto id = [n](int r, int c) { return r * n + c; };
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == '0') { ++water; continue; }
if (i > 0 && grid[i - 1][j] == '1')
uf.unite(id(i, j), id(i - 1, j));
if (j > 0 && grid[i][j - 1] == '1')
uf.unite(id(i, j), id(i, j - 1));
}
}
return uf.getCount() - water;
}
};
7.2 朋友圈 / 省份数量
题目:isConnected[i][j] = 1 表示 i 和 j 直接相连,求省份数。
#include <vector>
class UnionFind {
std::vector<int> parent, rank;
int count;
public:
UnionFind(int n) : parent(n), rank(n, 0), count(n) {
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return;
if (rank[rx] < rank[ry]) parent[rx] = ry;
else if (rank[rx] > rank[ry]) parent[ry] = rx;
else { parent[ry] = rx; ++rank[rx]; }
--count;
}
int getCount() const { return count; }
};
class Solution {
public:
int findCircleNum(std::vector<std::vector<int>>& isConnected) {
int n = isConnected.size();
UnionFind uf(n);
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j)
if (isConnected[i][j])
uf.unite(i, j);
return uf.getCount();
}
};
7.3 冗余连接
题目:n 个节点 n 条边的无向图,删掉一条边使其变成树。返回那条多余边。
#include <vector>
class UnionFind {
std::vector<int> parent, rank;
public:
UnionFind(int n) : parent(n), rank(n, 0) {
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
bool unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return false; // 已经在同集合 → 形成环
if (rank[rx] < rank[ry]) parent[rx] = ry;
else if (rank[rx] > rank[ry]) parent[ry] = rx;
else { parent[ry] = rx; ++rank[rx]; }
return true;
}
};
class Solution {
public:
std::vector<int> findRedundantConnection(
std::vector<std::vector<int>>& edges) {
int n = edges.size();
UnionFind uf(n + 1);
for (auto& e : edges) {
if (!uf.unite(e[0], e[1]))
return {e[0], e[1]};
}
return {};
}
};
解释:树有 n 个节点、n−1 条边。多出一条边后,遍历所有边,当发现两个节点已经连通时,这条边就是冗余的——它形成了环。
7.4 最长连续序列
题目:无序数组 [100, 4, 200, 1, 3, 2],最长连续序列 [1,2,3,4] 长度为 4。
并查集解法:连续的数合并,统计最大集合大小。
#include <vector>
#include <unordered_map>
#include <algorithm>
class UnionFind {
std::vector<int> parent, rank, size;
public:
UnionFind(int n) : parent(n), rank(n, 0), size(n, 1) {
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
int unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return size[rx];
if (rank[rx] < rank[ry]) {
parent[rx] = ry;
size[ry] += size[rx];
return size[ry];
} else if (rank[rx] > rank[ry]) {
parent[ry] = rx;
size[rx] += size[ry];
return size[rx];
} else {
parent[ry] = rx;
size[rx] += size[ry];
++rank[rx];
return size[rx];
}
}
};
class Solution {
public:
int longestConsecutive(std::vector<int>& nums) {
if (nums.empty()) return 0;
int n = nums.size();
UnionFind uf(n);
std::unordered_map<int, int> valToIdx;
int ans = 1;
for (int i = 0; i < n; ++i) {
if (valToIdx.count(nums[i])) continue;
valToIdx[nums[i]] = i;
if (valToIdx.count(nums[i] - 1))
ans = std::max(ans, uf.unite(i, valToIdx[nums[i] - 1]));
if (valToIdx.count(nums[i] + 1))
ans = std::max(ans, uf.unite(i, valToIdx[nums[i] + 1]));
}
return ans;
}
};
对比哈希解法:哈希法(存端点)O(n) 时间 O(n) 空间,代码更短。并查集解法思路更直观——连续的数天然"连通",但常数更大。面试中哈希法更推荐,但掌握并查集解法体现你举一反三的能力。
7.5 连通网络的操作次数
题目:n 台电脑,用网线 connections[i] = [a, b] 连接。求最少移动网线次数使所有电脑连通。
#include <vector>
class UnionFind {
std::vector<int> parent, rank;
int count;
public:
UnionFind(int n) : parent(n), rank(n, 0), count(n) {
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
bool unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return false;
if (rank[rx] < rank[ry]) parent[rx] = ry;
else if (rank[rx] > rank[ry]) parent[ry] = rx;
else { parent[ry] = rx; ++rank[rx]; }
--count;
return true;
}
int getCount() const { return count; }
};
class Solution {
public:
int makeConnected(int n, std::vector<std::vector<int>>& connections) {
if (connections.size() < n - 1) return -1; // 线不够
UnionFind uf(n);
int extra = 0;
for (auto& c : connections)
if (!uf.unite(c[0], c[1])) ++extra;
int need = uf.getCount() - 1; // 连通分量数 - 1
return need <= extra ? need : -1;
}
};
解释:n 台电脑连通至少需要 n−1 条线。多余的线可以移动。统计多余线和孤立分量数即可。
7.6 等式方程的可满足性
题目:给定 ["a==b","b!=c","c==a"],判断是否矛盾。
#include <vector>
#include <string>
class UnionFind {
std::vector<int> parent, rank;
public:
UnionFind(int n) : parent(n), rank(n, 0) {
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return;
if (rank[rx] < rank[ry]) parent[rx] = ry;
else if (rank[rx] > rank[ry]) parent[ry] = rx;
else { parent[ry] = rx; ++rank[rx]; }
}
bool connected(int x, int y) { return find(x) == find(y); }
};
class Solution {
public:
bool equationsPossible(std::vector<std::string>& equations) {
UnionFind uf(26); // 只有小写字母
// 第一遍:处理所有 ==
for (auto& eq : equations)
if (eq[1] == '=')
uf.unite(eq[0] - 'a', eq[3] - 'a');
// 第二遍:检查 != 是否矛盾
for (auto& eq : equations)
if (eq[1] == '!' && uf.connected(eq[0] - 'a', eq[3] - 'a'))
return false;
return true;
}
};
解释:先用 == 建立连通关系,再用 != 检查是否两个本应不同的变量已经被连通。若连通则矛盾。
8. 总结
并查集是少数能在教科书和面试题中保持完全一致实现的数据结构。背下模板,面试遇到"连通"、“集合”、“分组”、"检测环"这类关键词,直接套用。
关键记忆点:
- 路径压缩:find 时顺手把沿途节点挂到根上
- 按秩合并:矮树挂高树,避免树退化
- count 变量:用来统计连通分量数,很多题目直接返回这个值
反阿克曼函数的常数极小,可以用一辈子的数据结构。
1085

被折叠的 条评论
为什么被折叠?



