前言C
最近刷每日一题,连着两天出了并查集,分别是 547. 省份数量 和 1202. 交换字符串中的元素 下面以 547. 省份数量 为例,简单阐述下并查集。
原理
并查集是一种简洁实用的数据结构,通常用于判断两元素是否属于同一集合时使用。并查集只有两种操作,合并集合和查询集合。
并查集中引入了代表元素的概念,即用一个元素来代替整个集合。可以把并查集类比为一颗树,树是从上往下的有向图,并查集是从下往上的有向图。这样,一个树上的所有节点最终都能找到树的根节点,也就是说,集合的所有元素都能找到并查集的代表元素。
初始化
将每个元素的父节点初始化为自己。
1 2 3 4
| int fa[n]; for(int i = 0; i < n; i++) { fa[i] = i; }
|
并操作
并操作将两个集合合并到同一集合。只需要一个元素的代表元素(A)认另一个元素的代表元素(B)当爹就行。这样,A集合和B集合都变为了B集合,两个集合合并为了同一个集合。
1 2 3 4 5 6 7
| for(int i = 0; i < n; i++) { for(int j = 0; j < i; j++) { if(isConnected[i][j] == 1) { fa[findfa(fa,j)] = findfa(fa,i); } } }
|
查操作
查操作通常使用递归,不停地找父节点。直到找到一个节点的父节点是这个节点本身,那么这个节点就是待查元素的代表元素。
1 2 3 4 5 6
| int findfa(int *fa, int p) { if(fa[p] != p) { return findfa(fa,fa[p]); } return p; }
|
理解了并查集的基本操作,剩下就顺着题意写就完事。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class Solution { public: int findfa(int *fa, int p) { if(fa[p] != p) { return findfa(fa,fa[p]); } else { return p; } }
int findCircleNum(vector<vector<int>>& isConnected) { int n = isConnected.size(); if(n == 0) return 0; int fa[n]; int ans = 0; bool read[n]; memset(read, false, sizeof(read)); for(int i = 0; i < n; i++) { fa[i] = i; } for(int i = 0; i < n; i++) { for(int j = 0; j < i; j++) { if(isConnected[i][j] == 1) { fa[findfa(fa,j)] = findfa(fa,i); } } } for(int i = 0; i < n; i++) { int thisfa = findfa(fa,i); if(read[thisfa] == false) { ans++; read[thisfa] = true; } } return ans; } };
|
路径压缩
对于层数较高且查询的次数要求较高时,上面所示的并查集并不能高效地完成所需要求。因此我们引入了并查集的路径压缩。对于路径压缩,我们只需要修改并操作的代码,把构造一个棵树改为构造一棵层数为1的树仙人掌。在一般情况下,路径压缩的并查集把并操作的效率略微降低,查操作的效率提高,在刷题时可以取舍使用。
更多
并查集是应用极广的数据结构,最小生成树的Kruskal算法也常通过并查集来写。
Kruskal算法的主要思想:以边为主导地位,始终选择当前可用的最小边权的边(sort或priority_queue)并加入集合,下面展示Kruskal的核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| int father(int x) { if(fat[x] != x) { return father(fat[x]); } return x; }
void unionn(int x, int y) { fat[father(y)] = father(x); }
if(father(edge[i].from) != father(edge[i].to)) { unionn(edge[i].from, edge[i].to); tot += edge[i].dis; k++; }
|