天天看点

浅谈并查集

浅谈并查集

LaTeX \LaTeX LATE​X

传送门

定义

并查集是一种树形的数据结构,它可以用于维护多个不相交集合的合并、查询操作。并查集通常以森林的形式维护。我们通常将每个集合看作是一棵有根树。

基本操作

合并

对于每次合并元素 ( x , y ) (x, y) (x,y) 所在的集合的请求,我们分别找到 x , y x, y x,y 所在的有根树的根节点,并将 x x x 的根节点父节点置为 y y y 的根节点。

若我们需要动态查询合并后 x x x 所在的集合的大小,则合并时 x x x 所在的集合大小需要加上 y y y 所在的集合大小,即 s i z e [ x ] + = s i z e [ y ] size[x] += size[y] size[x]+=size[y] 。

查询

对于每次查询元素 ( x , y ) (x, y) (x,y) 是否在同一集合中的请求,我们分别找到 x , y x, y x,y 所在的有根树的根节点。由于同一集合内,所有元素所在的有根树的根节点相同,故而我们只需要判断 x x x 的根节点是否等于 y y y 的根节点即可。

路径压缩

对于每次查找 x x x 的祖宗结点,时间复杂度为 O ( h ) O(h) O(h) ,而我们可以查找完成后赋值,时间复杂度为 O ( 1 ) O(1) O(1) 。

初始化

对于任意 1 < = i < = n , f a [ i ] = 1 , s i z e [ i ] = 1 1 <= i <= n,fa[i] = 1,size[i] = 1 1<=i<=n,fa[i]=1,size[i]=1 。

并查集模板(洛谷 P3367)

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;

int u, v, fa[maxn];

//查询 + 优化 
int get(int x){
    if (fa[x] == x)return x;
    return fa[x] = get(fa[x]);//路径压缩
}

//合并 
void merge(int x, int y){
    x = get(x);
    y = get(y);
    if (x != y)
        fa[y] = x;
}

int main(){
    int n, m, k;
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1; i <= n; i++)fa[i] = i; 
    for (int i = 1; i <= m; i++){
        scanf("%d%d", &u, &v);
        merge(u, v);
    }
    for (int i = 1; i <= k; i++){
        scanf("%d%d", &u, &v);
        if (get(u) == get(v))printf("Yes\n");
        else printf("No\n");
    }
    return 0;
}
           

种类并查集

转载自 知乎 @Pecco

一般的并查集,维护的是具有连通性、传递性的关系,例如亲戚的亲戚是亲戚。但是,有时候,我们要维护另一种关系:敌人的敌人是朋友。种类并查集就是为了维护一种循环对称的关系而诞生的。

洛谷 P1525 关押罪犯

题目描述

S S S 城现有两座监狱,一共关押着 N N N 名罪犯,编号分别为 1 − N 1−N 1−N。他们之间的关系自然也极不和谐。很多罪犯之间甚至积怨已久,如果客观条件具备则随时可能爆发冲突。我们用“怨气值”(一个正整数值)来表示某两名罪犯之间的仇恨程度,怨气值越大,则这两名罪犯之间的积怨越多。如果两名怨气值为 c c c 的罪犯被关押在同一监狱,他们俩之间会发生摩擦,并造成影响力为 c c c 的冲突事件。

每年年末,警察局会将本年内监狱中的所有冲突事件按影响力从大到小排成一个列表,然后上报到 S S S 城 Z Z Z 市长那里。公务繁忙的 Z Z Z 市长只会去看列表中的第一个事件的影响力,如果影响很坏,他就会考虑撤换警察局长。

在详细考察了 N N N 名罪犯间的矛盾关系后,警察局长觉得压力巨大。他准备将罪犯们在两座监狱内重新分配,以求产生的冲突事件影响力都较小,从而保住自己的乌纱帽。假设只要处于同一监狱内的某两个罪犯间有仇恨,那么他们一定会在每年的某个时候发生摩擦。

那么,应如何分配罪犯,才能使 Z Z Z 市长看到的那个冲突事件的影响力最小?这个最小值是多少?

输入格式

每行中两个数之间用一个空格隔开。第一行为两个正整数 N , M N,M N,M,分别表示罪犯的数目以及存在仇恨的罪犯对数。接下来的 M M M 行每行为三个正整数 a j , b j , c j a_j,b_j,c_j aj​,bj​,cj​ ,表示 a j a_j aj​​ 号和 b j b_j bj​ 号罪犯之间存在仇恨,其怨气值为 c j c_j cj​ 。

数据保证 1 < a j ≤ b j ≤ N , 0 < c j ≤ 1 0 9 1<a_j\leq b_j\leq N, 0 < c_j\leq 10^9 1<aj​≤bj​≤N,0<cj​≤109 ,且每对罪犯组合只出现一次。

输出格式

共 1 1 1 行,为 Z Z Z 市长看到的那个冲突事件的影响力。如果本年内监狱中未发生任何冲突事件,请输出 0 0 0。

其实很容易想到,这里可以贪心,把所有矛盾关系从大到小排个序,然后尽可能地把矛盾大的犯人关到不同的监狱里,直到不能这么做为止。这看上去可以用并查集维护,但是有一个问题:我们得到的信息,不是哪些人应该在相同的监狱,而是哪些人应该在不同的监狱。这怎么处理呢?这个题其实有很多做法,但这里,我们介绍使用种类并查集的做法。

我们开一个两倍大小的并查集。例如,假如我们要维护4个元素的并查集,我们改为开8个单位的空间:

浅谈并查集

我们用 1 ∼ 4 1 \sim 4 1∼4 维护朋友关系(就这道题而言,是指关在同一个监狱的狱友),用5~8维护敌人关系(这道题里是指关在不同监狱的仇人)。现在假如我们得到信息: 1 1 1 和 2 2 2 是敌人,应该怎么办?

我们

merge(1, 2+n), merge(1+n, 2)

。这里 n n n 就等于4,但我写成 n n n 这样更清晰。对于 1 1 1 个编号为 i i i 的元素, i + n i+n i+n 是它的敌人。所以这里的意思就是: 1 1 1 是 2 2 2 的敌人, 2 2 2 是 1 1 1 的敌人。

浅谈并查集

现在假如我们又知道 2 2 2 和 4 4 4 是敌人,我们

merge(2, 4+n), merge(2+n, 4)

;

浅谈并查集

发现了吗,敌人的敌人就是朋友, 2 2 2 和 4 4 4 是敌人, 2 2 2 和 1 1 1 也是敌人,所以这里, 1 1 1 和 4 4 4 通过 2 + n 2+n 2+n 这个元素间接地连接起来了。这就是种类并查集工作的原理。

A C   c o d e AC \ code AC code

#include<bits/stdc++.h>
using namespace std;
int read() //快速读入,可忽略
{
    int ans = 0;
    char c = getchar();
    while (!isdigit(c))
        c = getchar();
    while (isdigit(c))
    {
        ans = (ans << 3) + (ans << 1) + c - '0';
        c = getchar();
    }
    return ans;
}
struct data  //以结构体方式保存便于排序
{
    int a, b, w;
} C[100005];
int cmp(data &a, data &b)
{
    return a.w > b.w;
}
int fa[40005], rank[40005];  //以下为并查集
int find(int a)
{
    return (fa[a] == a) ? a : (fa[a] = find(fa[a]));
}
int query(int a, int b)
{
    return find(a) == find(b);
}
void merge(int a, int b)
{
    int x = find(a), y = find(b);
    if (rank[x] >= rank[y])
        fa[y] = x;
    else
        fa[x] = y;
    if (rank[x] == rank[y] && x != y)
        rank[x]++;
}
void init(int n)
{
    for (int i = 1; i <= n; ++i)
    {
        rank[i] = 1;
        fa[i] = i;
    }
}
int main()
{
    int n = read(), m = read();
    init(n * 2); //对于罪犯i,i+n为他的敌人
    for (int i = 0; i < m; ++i)
    {
        C[i].a = read();
        C[i].b = read();
        C[i].w = read();
    }
    std::sort(C, C + m, cmp);
    for (int i = 0; i < m; ++i)
    {
        if (query(C[i].a, C[i].b))  //试图把两个已经被标记为“朋友”的人标记为“敌人”
        {
            printf("%d\n", C[i].w); //此时的怒气值就是最大怒气值的最小值
            break;
        }
        merge(C[i].a, C[i].b + n);
        merge(C[i].b, C[i].a + n);
        if (i == m - 1)  //如果循环结束仍无冲突,输出0
            puts("0");
    }
    return 0;
}
           

继续阅读