算法竞赛进阶指南0x41 并查集

        并查集(Disjoint-Set)是一种可以动态维护若干个不重叠的集合,并支持合并与查询的数据结构。详细地说,并查集包括如下两个基本操作:

1.Get:查询一个元素属于哪一个集合。

2.Merge:把两个集合合并成一个大集合。

        为了具体实现并查集这种数据结构,我们首先需要定义的表示方法。在并查集中,我们采用“代表元"法,即为每个集合选择一个固定的元素,作为整个集合的"代表"。

        其次,我们要定义归属关系的表示方法:使用一个树形结构存储每个集合,树上的每个节点都是一个元素,树根是集合的代表元素。

        接下来,我们对并查集的具体代码实现作一下具体说明:

路径压缩:

最简单的并查集效率是比较低的。例如,来看下面这个场景:

算法竞赛进阶指南0x41 并查集_第1张图片

现在我们要merge(2,3),于是从2找到1,fa[1]=3,于是变成了这样: 

算法竞赛进阶指南0x41 并查集_第2张图片

然后我们又找来一个元素4,并需要执行merge(2,4): 

算法竞赛进阶指南0x41 并查集_第3张图片

从2找到1,再找到3,然后fa[3]=4,于是变成了这样: 

算法竞赛进阶指南0x41 并查集_第4张图片

大家应该有感觉了,这样可能会形成一条长长的,随着链越来越长,我们想要从底部找到根节点会变得越来越难。

怎么解决呢?我们可以使用路径压缩的方法。既然我们只关心一个元素对应的根节点,那我们希望每个元素到根节点的路径尽可能短,最好只需要一步,像这样:

算法竞赛进阶指南0x41 并查集_第5张图片

其实这说来也很好实现。只要我们在查询的过程中,把沿途的每个节点的父节点都设为根节点即可。下一次再查询时,我们就可以省很多事。这用递归的写法很容易实现。

1.并查集的存储: 

        使用一个数组fa,保存父节点(根的父节点设为自己)。

int fa[SIZE];

2.并查集的初始化:

        设有n个元素,起初所有元素各自构成一个独立的集合,即有n颗1个点的树。 

    for (int i = 1; i <= n; i++) {
        fa[i] = i;
    }

3.并查集的Get操作:

        若x是树根,则x就是集合代表,否则递归访问fa[x]直至根节点。 

int get(int x) {
    if (fa[x] == x) {
        return x;
    }
    return fa[x] = get(fa[x]); //路径压缩,fa直接赋值为代表元素
}

4.并查集的Merge操作:

        合并元素 x 和元素 y所在的集合,等价于让 x 的树根作为 y 的树根的子节点。 

void merge(int x, int y) {
    fa[get(x)] = get(y);
}

例题:程序自动分析

tag:离散化,并查集,排序        

思路:对于"变量相等"的约束条件,我们将其放入同一个集合里面,对于"变量不相等"的约束条件,我们看两个元素是否在同一个集合里面(根相同),如果在同一个集合里面,无法满足既相等要不相等的互斥条件,所以我们要输出NO,全部都不冲突的话,我们输出YES。但是我们要注意:

要先把所有的"变量相等"的数据排在前面,最后判断不相等的情况,其次,因为x和y的数据范围太大,无法开那么大的fa数组,所以我们要对数据离散化处理。具体代码如下:

#include 
#define i64 long long

constexpr int N = 1e6;

int fa[N];

int find(int x) {
    return fa[x] == x ? x : (fa[x] = find(fa[x]));
}

void merge(int x, int y) {
    fa[find(x)] = find(y);
}

struct node {
    int x, y, opt;
};

bool cmp(node a, node b) {
    return a.opt > b.opt;
}//排序

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int t;
    std::cin >> t;
    while (t--) {

        int n;
        std::cin >> n;
        std::vector a(n + 1);
        std::vector POS;

        std::memset(fa, 0, sizeof fa);

        for (int i = 1; i <= n; i++) {
            std::cin >> a[i].x >> a[i].y >> a[i].opt;
            POS.emplace_back(a[i].x);
            POS.emplace_back(a[i].y);
        }

        std::sort(POS.begin(), POS.end());

        POS.erase(std::unique(POS.begin(), POS.end()), POS.end());

        for (int i = 1; i <= n; i++) {
            a[i].x = 
        std::lower_bound(POS.begin(), POS.end(), a[i].x) - POS.begin();
            a[i].y = 
        std::lower_bound(POS.begin(), POS.end(), a[i].y) - POS.begin();
        }//离散化
        
        int cnt = (int)POS.size();

        for (int i = 1; i <= cnt; i++) {
            fa[i] = i;
        }//初始化

        std::sort(a.begin() + 1, a.end(), cmp);

        bool f = false;

        for (int i = 1; i <= n; i++) {
            if (a[i].opt == 1) {
                merge(a[i].x, a[i].y);
            } else {
                if (find(a[i].x) == find(a[i].y)) {
                    f = true;
                    break;
                    //冲突
                }
            }
        }
    
        if (f) {
            std::cout << "NO\n";
        } else {
            std::cout << "YES\n";
        }//注意大小写

    }

    return 0;
}

"扩展域" 与 "边带权" 的并查集

        并查集实际上是由若干颗树构成的森林,我们可以在树中的每条边上记录一个权值,即维护一个数组d,用d[x]保存节点 x 到父节点fa[x]之间的边权,在每次路径压缩后,每次访问过的节点都会直接指向树根,如果我们同时更新这些节点的 d 值,就可以利用路径压缩过程来统计每个节点到树根之间的路径上的一些信息。这就是所谓的"边带权"的并查集。

例题:银河英雄传说

        一条链也是一棵树,只不过是树的特殊形态。因此可以把每一列战舰看作一个集合,用并查集维护。最初,N个战舰构成N个独立的集合。

        在没有路径压缩的情况下,fa[x]就表示排在第 x 号战舰前面的那个战舰的编号。一个集合的代表就是位于最前边的战舰。另外,让树上每条边带权值1,这样树上两点之间的距离减一就是二者之间间隔的战舰数量。

        在考虑路径压缩的情况下,我们额外建立一个数组dist,dist[x]记录战舰 x 与 fa[x]之间的边的权值。在路径压缩把 x 直接指向树根的同时,我们把 dist[x] 更新为从 x 到树根的路径上的所有边权之和。下面的代码对 find 函数稍加修改,即可实现对 dist数组的维护:

int get(int x) {
    if (x == fa[x]) {
        return x;
    }
    int root = get(fa[x]);
    dist[x] += dist[fa[x]];
    return fa[x] = root;
}

        当收到一个指令C x y时,分别执行 get(x) 和 get(y) 完成查询和路径压缩。若二者的返回值相同,则说明,x 和 y处在同一列中。因为 x 和 y此时都已经指向树根,所以dist[x]保存了位于x之前的战舰数量,d[y]保存了位于y之前的战舰数量。二者之差的绝对值再减1,就是x 和 y之间间隔的战舰数量。 

        当收到一个M x y指令时,把x 的树根作为 y 的树根的子节点,连接的新边的权值应该设为合并之前集合y的大小 (根据题意,集合y中的全部战舰都排在集合x之前)。因此,我们还需要一个 size 数组在每个树根上记录集合大小。下面这段对Merge函数稍加修改的代码实现了这条指令:

#include 
#define i64 long long

constexpr int N = 5e5 + 10;

int fa[N], dist[N], size[N];

int get(int x) {
    if (fa[x] == x) {
        return x;
    }
    int root = get(fa[x]);
    //递归计算集合代表
    dist[x] += dist[fa[x]];
    //维护dist数组 -> 对边权求和
    return fa[x] = root;
    // 路径压缩
}

void merge(int x, int y) {
    x = get(x), y = get(y);
    fa[x] = y, dist[x] = size[y];
    size[y] += size[x];
}

int main() {
    
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int t;
    std::cin >> t;

    for (int i = 1 ; i <= t; i++) {
        fa[i] = i, size[i] = 1;
    }//初始化

    while (t--) {
        char op;
        int i, j;
        std::cin >> op >> i >> j;
        if (op == 'M') {
            merge(i, j);
        } else {
            int fi = get(i), fj = get(j);
            if (fi != fj) {
                std::cout << "-1\n";
                //不在同一列
            } else {
                std::cout << std::abs(dist[i] - dist[j]) - 1 << "\n";
            }
        }
    }

    return 0;
}

你可能感兴趣的:(算法竞赛进阶指南,算法,数据结构,c++)