并查集的应用

 

一,概述

并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。

它虽然不是很复杂的数据类型,却也是设计的很精妙,可以运用于许多地方。

 

 

二,集合的个数

1,集合的个数:即为树的个数。

假设有 n 个点,m 条边,则初始时有 n 个集合,则每当两个点 join 时:

返回 1,代表两个点所在的集合不是同一个,两个集合可以合并。集合的个数由 2 变成 1,即集合的个数减少 1 个。

返回 0,代表两个点所在的集合是同一个,无需合并,集合的个数不变。

即:

  集合的个数 = n - (join(每一条边) == 1 的个数)

 2,之前遇到过这题型,现在找不到了,例题就先空着吧,遇到了再补。

 

 

三,环的个数

1,环的个数:根据所给的图(点和边),确定图中有几个环。

给定的图,必须限定图中的边无交叉(否则可能一次形成多个环),所算出来的环的个数也只是计算图中最小的环的个数(不包括多个环叠加起来的大的环那种),否则无法用并查集计算。

假设有 n 个点,m 条边,每当两个点 join 时:

返回 1,代表没有形成新的环。

返回 0,代表形成一个新的环。

即:

  环的个数 = (join(每一条边) == 0 的个数)

注意到:

   (join(每一条边) == 0 的个数) + (join(每一条边) == 1 的个数) = m

所以有:

  集合的个数 - 环的个数 = n - m

2,如:HDU 2120 ice_cream's world I

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 1000000+10
int p[N], n, m;
int find(int x)
{
    if (p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}
int join(int x, int y)
{
    x = find(x), y = find(y);
    if (x == y)
        return 1;
    p[x] = y;
    return 0;
}
int main(void)
{
    while (scanf("%d%d", &n, &m) != EOF)
    {
        for (int i = 0; i <= n; i++)
            p[i] = i;

        int ans = 0;
        while (m--)
        {
            int a, b;
            scanf("%d%d", &a, &b);
            ans += (join(a, b));
        }
        printf("%d\n", ans);
    }

    system("pause");
    return 0;
}
View Code

 

 

四,集合中点的个数

1,确定与某个特定的点连通的点的个数。

2,如 POJ 1611 The Suspects,就是要求和点0连通的所有点的个数。该问题可以转化为求点0所在集合的个数。有因为每个集合都有一个代表性的点:根节点。

于是,设数组 num[],num[i] 表示以 i 为根节点的集合的点的个数。

算法开始前,将 num[i] = 1; 表示每个集合初始时只有一个点。

算法运行时,在集合合并结束后,将 num[根节点]++; 就可以了。

算法结束后,查询时,num[find(x)] 就表示点 x 所在的集合的点的个数。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int p[30000 + 5], num[30000 + 5],  a[30000 + 5];
int n, m;
int find(int x)
{
    if (x != p[x])
        p[x] = find(p[x]);
    return p[x];
}
void join(int x, int y)
{
    x = find(x), y = find(y);
    if (x == y)
        return;
    p[x] = y;
    num[y] += num[x];
}
int main(void)
{
    while (scanf("%d%d", &n, &m), n + m)
    {
        for (int i = 0; i < n; i++)
        {
            p[i] = i;
            num[i] = 1;
        }
        while (m--)
        {
            int t; scanf("%d", &t);
            for (int i = 0; i < t; i++)
            {
                scanf("%d", &a[i]);
                if (i != 0)
                    join(a[i], a[i - 1]);
            }
        }
        printf("%d\n", num[find(0)]);
    }
    system("pause");
    return 0;
}
View Code

 

 

五,集合的含义

1,点x 和 点y 存在不同的关系时,可以用不同的并查集表示不同的关系。

2,如 POJ 1182 食物链里 x 和 y 是有三种不同的对应关系的:y 是 x 的同类;y 是 x 的食物;y 是 x 的天敌。

那么,如何写出三个不同的并查集呢?这里有个小技巧,就是我们在题目中用到的数值都是有取值范围的,我们将其最大值设为 n。显然 x 和 y 的值都是小于等于 N。

我们设 x <= [1, n],y <= [1, n],于是有:

第一个并查集:其中的任意两个点,我们是区分不出哪个是 x,哪个是 y,但无所谓,第一个并查集可以用来表示同类。无论是 x 和 y 是同类,还是 y 和 x 是同类都是相同的意义。

第二个并查集:其中有些数的取值范围是 [1, n],表示 y;有些数的取值范围是 [1+n, n*2],表示 x。该集合可以用来表示 y 是 x 的食物。

第三个并查集:其中有些数的取值范围是 [1, n],表示 y;有些数的取值范围是 [1+n*2, n*3],表示 x。该集合可以用来表示 y 是 x 的天敌。

于是,我们就可以用 find函数,来判断 x,y 的关系了。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#define N 50000+5
int p[N * 3];   // g++ 不能这样?只能 int p[200000+5] 或 p[(50000+5)*3]
                // 同类,猎物,天敌
int find(int x)
{
    int a = x;
    while (x != p[x])
        x = p[x];
    while (x != a)
    {
        int t = p[a];
        p[a] = x;
        a = t;
    }
    return x;
}
void join(int x, int y)
{
    x = find(x), y = find(y);
    if (x == y)
        return;
    p[x] = y;
}
int main(void)
{
    int n, k;
    scanf("%d %d", &n, &k);
    {
        for (int i = 1; i <= n * 3 + 2; i++)
            p[i] = i;

        int ans = 0;
        while (k--)
        {
            int d, x, y;
            scanf("%d %d %d", &d, &x, &y);
            if (x > n || y > n)
            {
                ans++; 
                continue;
            }
            if (d == 1)
            {
                // x 的猎物是 y  或 x 的天敌是 y
                if (find(x + n) == find(y) || find(x + 2 * n) == find(y))
                {
                    ans++;
                    continue;
                }
                // x 的同类是 y
                join(x, y);
                join(x + n, y + n);
                join(x + 2 * n, y + 2 * n);
            }
            else  // x 吃 y
            {
                if (x == y)
                {
                    ans++;
                    continue;
                }
                // x 的同类是 y 或者 y 的猎物是 x
                if (find(x) == find(y) || find(x) == find(y + n))
                {
                    ans++; continue;
                }
                join(x + n, y);    // x 的猎物是 y
                join(x, y + 2 * n);   // x 的天敌是 y
                join(x + 2 * n, y + n);   // x的天敌 是 y的猎物 
            }
        }
        printf("%d\n", ans);
    }
    system("pause");
    return 0;
}
View Code

 

 

六,移动次数

1,如 HDU 3635 Dragon Balls

初始状态下:第 i 颗龙珠对应第 i 个城市。然后就是给你若干移动,即把第 i 颗龙珠所在的城市的所有龙珠移动到第 j 颗龙珠所在的城市。其实就是集合的合并。

这一题主要难点在于它会问,第 i 颗球的移动次数。这个要怎么理解呢?

我们设数组 mov[],将其初始化为 0,表示所有珠子都没有经过移动。然后,在 join 的时候,mov[移动的龙珠所在的集合的根节点]++; 此时,我们得到的 mov[] 数组,它只记录了移动时龙珠所在集合的根节点移动次数,其它同集合的移动次数都没有增加。

那么,如何让同集合的其它点的移动次数增加呢?

我们可以在压缩路径的递归算法,回溯的时候增加。怎么个增加法呢?

你那个回溯的时候,不正是从根节点往叶节点回溯。而每次回溯时,移动次数都是存在根节点中,而压缩路径又有延迟性,有可能你根节点进行了第二次移动变成了不是根节点,但之前的移动次数还保留着。所以,某个点的移动次数 = 该点到根节点的路径上所有节点的移动次数之和。所以你需要利用回溯,把所有节点的移动次数从根节点加下来。

最后,有一点需要注意,还是因为压缩路径。如果你要找第 i 颗球的移动次数,但有可能该颗球还没进行压缩路径,即没有计算别的球移动对它的移动次数的影响,所以回答 mov[i] 之前需要 find(i) 计算一下。

2,总结一下:就是将某个点对同集合的其它所有点的影响保存在当前根节点中(因为该根节点在之后集合的和合并后可能不是根节点了),然后利用路径压缩,将影响传递给所有同集合的点。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#define N 10005
int p[N], num[N], mov[N];
int n, m;
int find(int x)
{
    if (p[x] != x)
    {
        int t = p[x];
        p[x] = find(p[x]);
        mov[x] += mov[t];
    }
    return p[x];
}
void join(int x, int y)
{
    x = find(x), y = find(y);
    if (x == y)
        return;
     p[x] = y;
     num[y] += num[x];
     mov[x]++;
}
int main(void)
{
    int t; scanf("%d", &t);
    int ci = 0;
    while (t--)
    {
        scanf("%d%d", &n, &m);
        for (int i = 0; i <= n; i++)
        {
            p[i] = i;
            num[i] = 1;
            mov[i] = 0;
        }

        printf("Case %d:\n", ++ci);
        char str[10];
        while (m--)
        {
            scanf("%s", str);
            int x, y;
            if (str[0] == 'T')
            {
                scanf("%d%d", &x, &y);
                join(x, y);
            }
            if (str[0] == 'Q')
            {
                scanf("%d", &x);
                int k = find(x);
                printf("%d %d %d\n", k, num[k], mov[x]);
            }
        }
    }

    system("pause");
    return 0;
}
View Code

 

 

 

=========== ========= ======== ======= ====== ====== ===== === == =

  《沙扬娜拉》  徐志摩

          ——赠日本女郎

  最是那一低头的温柔,

  像一朵水莲花不胜凉风的娇羞,

  道一声珍重,道一声珍重,

  那一声珍重里有蜜甜的忧愁——

 

posted @ 2020-03-25 14:31  叫我妖道  阅读(468)  评论(0编辑  收藏  举报
~~加载中~~