并查集专题 杭电OJ1232 灵巧求并和路径压缩

并查集也叫不相交集合,可以描述把一个集合通过等价关系(满足自反性、对称性和传递性的关系)划分为若干个等价类这一过程。并查集只有两个操作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章 不相交集类

上一篇:支付宝支付接口的调用(转)


下一篇:Oracle日志性能查看