并查集处理的是集合之间的关系,即‘union' , 'find' 。在这种数据类型中,N个不同元素被分成若干个组,每组是一个集合,这种集合叫做分离集合。并查集支持查找一个元素所属的集合和两个元素分别所属的集合的合并。
并查集支持以下操作:
MAKE(X):建立一个仅有成员X的新集合。
UNION(X,Y):将包含X和Y的动态集合合并为一个新集合S,此后该二元素处于同一集合。
FIND(X):返回一个包含X的集合。
注意:并查集只能进行合并操作,不能进行分割操作。
并查集的实现原理
并查集是使用树结构实现的:
1.初始化:准备N个节点来表示N个元素,最开始没有边。
2.合并:从一个组的根向另一个组的根连边,这样两棵树就变为了一棵树,也就把两个组合并为一个组了。
3.查询:查询两个节点是否在同一个组,只需要查询他们是否具有相同的根。
注意:
(1) 为避免树的退化,对于每棵树,记录其高度rank。
(2) 如果合并时两棵树高度不同,那么从rank小的向rank大的连边。
(3) 对于每个节点,一旦向上走到了一次根节点,就把这个节点到父亲的边改为直接连向根。不仅指查询的节点,也包括查询过程中经过的节点。使用这种简化的方法时,即使树的高度发生了改变,我们也不修改rank值。
并查集的复杂度
对N个元素并查集进行的一次操作的复杂度是O(α(N)),α(N)是阿克曼函数的反函数。这个复杂度是比O(LogN)还要快的。
并查集的具体C++实现:
#pragma once #include <cstring> const int MAX_N = 100000; class unite_find_set { private: int par[MAX_N]; int rank[MAX_N];//增加rank变量来防止树的退化(尽量让层数低的树连接到层数高的树上) public: unite_find_set(int n = 0) { init(n); } void init(int n) { for (int i = 0;i < n;++i) { par[i] = i; rank[i] = 0; } } int find(int x) { if (par[x] == x) return x; //此部分为两部分,find(par[x])为回溯寻找根节点,par[x]=回溯结果是把 //叶子节点直接连接到根节点上以实现路径压缩,为简化起见,路径压缩后 //我们没有更改rank值 else return par[x] = find(par[x]); } void unite(int x, int y) { x = find(x); y = find(y); if (x == y) return; if (rank[x] < rank[y]) par[x] = y; else { par[y] = x; if (rank[x] == rank[y]) rank[x]++; } } bool same(int x, int y) { return find(x) == find(y); } }
并查集的应用实例
(因为本人太懒,以下代码并未输入数据测试,但我有迷之自信没有问题)
更新:果然是迷之自信,已经发现问题了,已修复。
Kruskal最小生成树生成法:
Kruskal算法的简述见Prime算法的简述这篇文章有简单说明。
#include <algorithm> #include "union_find_set.h" const int MAX_V = 100; const int INF = 1000000; int cost[MAX_V][MAX_V]; int d[MAX_V], V; struct Edge { int from, to, cost; } edge[MAX_N];//用结构体表示边 bool compare(const Edge &a, const Edge &b) { return a.cost < b.cost; } bool path[MAX_V][MAX_V];//记录结果 int res; void Kruskal() { res = 0; union_find_set set(V);//初始化并查集 int p = 0;//初始化edge数组游标 for (int i = 0;i < V;++i) { for (int j = 0;j < V;++j) { path[i][j] = false;//给结果数组赋值 if (cost[i][j] != INF) { edge[p++] = { i,j,cost[i][j] };//给edge数组赋值 } } } std::sort(edge, edge + p, compare);//按边权从小到大排序edge数组 for (int i = 0;i < p;++i) { if (!set.same(edge[i].from, edge[i].to))//如果边的首尾节点没有在一个生成树中 { path[edge[i].from][edge[i].to] = true;//添加这条边进入生成树 set.unite(edge[i].from, edge[i].to);//让首尾节点连接 res += edge[i].cost; } }