并查集也叫不相交集合,可以描述把一个集合通过等价关系(满足自反性、对称性和传递性的关系)划分为若干个等价类这一过程。并查集只有两个操作find和union: find返回一个元素所属的等价类,union合并两个元素所在的等价类。
以杭电OJ1232题为例,说明并查集的两种实现和后一种实现的两种优化方法。
第一种实现,数组的每个元素储存它所在等价类的名字。find历程只要返回元素坐标的值就好了;而union历程则遍历数组进行修改。
对于连续的N次find操作或union操作,这种实现需要的时间分别为O(N)和O(N^2)
#include<bits/stdc++.h>
using namespace std;
int bjset[1010];
int bjset_find(int x)
{
return bjset[x];
}
void bjset_union(int x, int y,int n)
{
int classx = bjset_find(x);
int classy = bjset_find(y);
if (classx != classy) {
for (int i = 1; i <= n; ++i) {
if (bjset[i] == classx)
bjset[i] = classy;
}
}
}
int main()
{
#ifdef LOCAL
freopen("input.txt", "r", stdin);
#endif
int n;
while (cin >> n, n) {
for (int i = 1; i <= n; ++i) bjset[i] = i;//初始化并查集
int m; cin >> m;
while (m--) {
int x, y; cin >> x >> y;
bjset_union(x, y, n);
}
int ctr = 0;
for (int i = 1; i <= n; ++i) {
if (bjset[i] == i) ++ctr;
}
cout << ctr - 1 << endl;
}
return 0;
}
第二种实现,用树的结构,根节点指向自己。find历程只要迭代以返回根节点就可以了,union历程也只要合并根节点。
对于连续的N次find操作或union操作,这种实现需要的时间最坏情况分别为O(N^2)和O(N),但是由于树退化成链表的情况很少出现,因此这种实现比上一种实现更快,并且我们还有两种方法来优化这种实现。
#include<bits/stdc++.h>
using namespace std;
int bjset[1010];
int bjset_find(int x)
{
while (bjset[x] != x) x = bjset[x];
return bjset[x];
}
void bjset_union(int x, int y)
{
int rootx = bjset_find(x);
int rooty = bjset_find(y);
if (rootx != rooty) {
bjset[rootx] = rooty;
}
}
int main()
{
#ifdef LOCAL
freopen("input.txt", "r", stdin);
#endif
int n;
while (cin >> n, n) {
for (int i = 1; i <= n; ++i) bjset[i] = i;
int m; cin >> m;
while (m--) {
int x, y; cin >> x >> y;
bjset_union(x, y);
}
int ctr = 0;
for (int i = 1; i <= n; ++i) {
bjset[i] == i ? (++ctr) : 0;
}
cout << ctr - 1 << endl;
}
}
第一种优化方法叫灵活求并,优化了union历程中树的合并,使得树的高度得到控制。我们可以让节点指向负值来表示节点是根节点,并且用负的多少来表示这棵树的元素多少或树的高度(这两种策略分别叫做按大小的灵活求并和按高度/秩的灵活求并),每次union操作的时候总是让绝对值小的树的根指向大的树的根。
下面的代码用根节点指向负值的策略来使存储更多信息成为可能。
#include<bits/stdc++.h>
using namespace std;
int bjset[1010];
int bjset_find(int x)
{
while (bjset[x] >= 0) x = bjset[x];//find的写法有变化
return x;
}
void bjset_union(int x, int y)
{
int rootx = bjset_find(x);
int rooty = bjset_find(y);
if (rootx != rooty) {
bjset[rootx] = rooty;
}
}
int main()
{
#ifdef LOCAL
freopen("input.txt", "r", stdin);
#endif
int n;
while (cin >> n, n) {
for (int i = 1; i <= n; ++i) bjset[i] = -1;//初始化的方法不同
int m; cin >> m;
while (m--) {
int x, y; cin >> x >> y;
bjset_union(x, y);
}
int ctr = 0;
for (int i = 1; i <= n; ++i) {
bjset[i] == -1 ? (++ctr) : 0;
}
cout << ctr - 1 << endl;
}
}
灵活求并,主要是union例程的变化
#include<bits/stdc++.h>
using namespace std;
int bjset[1010];
int bjset_find(int x)
{
while (bjset[x] >= 0) x = bjset[x];
return x;
}
void bjset_union(int x, int y)
{
int rootx = bjset_find(x);
int rooty = bjset_find(y);
if (rootx == rooty) return;
if (bjset[rootx] > bjset[rooty]) {
bjset[rootx] = rooty;
(bjset[rooty])--;
}
else if (bjset[rootx] <= bjset[rooty]) {
bjset[rooty] = rootx;
(bjset[rootx])--;
}
}
int main()
{
#ifdef LOCAL
freopen("input.txt", "r", stdin);
#endif
int n;
while (cin >> n, n) {
for (int i = 1; i <= n; ++i) bjset[i] = -1;
int m; cin >> m;
while (m--) {
int x, y; cin >> x >> y;
bjset_union(x, y);
}
int ctr = 0;
for (int i = 1; i <= n; ++i) {
bjset[i] < 0 ? (++ctr) : 0;
}
cout << ctr - 1 << endl;
}
}
第二种优化的方法叫路径压缩,其思路是每次进行find例程的时候都让路径上的节点指向根节点,从而使树的高度得到减少。下面的两段代码把路径压缩和灵巧求并写在一起了,分别是路径压缩的迭代实现和递归实现。
#include<bits/stdc++.h>
using namespace std;
int bjset[1010];
int bjset_find(int x)//递归实现
{
if (bjset[x] > 0) bjset[x] = bjset_find(bjset[x]);
if (bjset[x] < 0) return x;
else return bjset[x];
}
void bjset_union(int x, int y)
{
int rootx = bjset_find(x);
int rooty = bjset_find(y);
if (rootx == rooty) return;
if (bjset[rootx] > bjset[rooty]) {
bjset[rootx] = rooty;
(bjset[rooty])--;
}
else if (bjset[rootx] <= bjset[rooty]) {
bjset[rooty] = rootx;
(bjset[rootx])--;
}
}
int main()
{
#ifdef LOCAL
freopen("input.txt", "r", stdin);
#endif
int n;
while (cin >> n, n) {
for (int i = 1; i <= n; ++i) bjset[i] = -1;
int m; cin >> m;
while (m--) {
int x, y; cin >> x >> y;
bjset_union(x, y);
}
int ctr = 0;
for (int i = 1; i <= n; ++i) {
bjset[i] < 0 ? (++ctr) : 0;
}
cout << ctr - 1 << endl;
}
}
#include<bits/stdc++.h>
using namespace std;
int bjset[1010];
int bjset_find(int x)//迭代实现
{
int r = x;
while (bjset[r] >= 0) r = bjset[r];
while (bjset[x] >= 0) {
int tmp = x;
x = bjset[x];
bjset[tmp] = r;
}
return r;
}
void bjset_union(int x, int y)
{
int rootx = bjset_find(x);
int rooty = bjset_find(y);
if (rootx == rooty) return;
if (bjset[rootx] > bjset[rooty]) {
bjset[rootx] = rooty;
(bjset[rooty])--;
}
else if (bjset[rootx] <= bjset[rooty]) {
bjset[rooty] = rootx;
(bjset[rootx])--;
}
}
int main()
{
#ifdef LOCAL
freopen("input.txt", "r", stdin);
#endif
int n;
while (cin >> n, n) {
for (int i = 1; i <= n; ++i) bjset[i] = -1;
int m; cin >> m;
while (m--) {
int x, y; cin >> x >> y;
bjset_union(x, y);
}
int ctr = 0;
for (int i = 1; i <= n; ++i) {
bjset[i] < 0 ? (++ctr) : 0;
}
cout << ctr - 1 << endl;
}
}
参考文献
《数据结构与算法分析——C++语言描述》(第四版) Mark Allen Weiss 第8章 不相交集类