【数据结构】扩展域并查集

上文链接

一、扩展域并查集

普通的并查集只能解决各元素之间仅存在一种相互关系,比如在普通并查集的《亲戚》这道题中:

  • a a a b b b 是亲戚关系, b b b c c c 是亲戚关系,这时就可以查找出 a a a c c c 也存在亲戚关系。

但如果存在各元素之间存在多种相互关系,普通并查集就无法解决。比如下面的案例:

  • a a a b b b 是敌人关系, b b b c c c 是敌人关系,那么 a a a c c c 的关系是什么呢?有句话说得好:敌人的敌人是朋友。所以 a a a c c c 其实不是敌人关系,而是一种朋友关系。

此时,就不仅仅是简单的敌人关系,还是出现一种朋友关系。

解决这类有多种关系的问题就需要对并查集进行扩展:**将每个元素拆分成多个域,每个域代表一种状态或者关系。**通过维护这些域之间的关系,来处理复杂的约束条件。

比如现在有 n n n 个人,它们之间可能是朋友关系可能是敌人关系,普通并查集我们会开辟一个大小为 n n n 的数组来维护它们之间的关系,现在它们的关系有两种,我们我们可以把数组扩大成两倍,开辟一个大小为 2 n 2n 2n 的数组, [ 1 , n ] [1, n] [1,n] 这个区间表示朋友域, [ n + 1 , 2 n ] [n + 1,2n] [n+1,2n] 区间表示敌人域,接下来:

  • 如果 x x x y y y 是朋友( 1 ≤ x , y ≤ n 1 \le x, y \le n 1x,yn),那么正常处理,把 x x x y y y 合并成一个集合;
  • 如果 x x x y y y 是敌人,那么我们把 x x x y + n y + n y+n 合并成一个集合,表示 x x x y y y 是敌人,注意我们也需要把 x + n x + n x+n y y y 合并成一个集合。

这样就可以利用两个域,将两种关系维护起来。


二、OJ 练习

1. 团伙 ⭐⭐⭐

【题目链接】

[P1892 BalticOI 2003] 团伙 - 洛谷

【题目描述】

现在有 n n n 个人,他们之间有两种关系:朋友和敌人。我们知道:

  • 一个人的朋友的朋友是朋友
  • 一个人的敌人的敌人是朋友

现在要对这些人进行组团。两个人在一个团体内当且仅当这两个人是朋友。请求出这些人中最多可能有的团体数。

【输入格式】

第一行输入一个整数 n n n 代表人数。

第二行输入一个整数 m m m 表示接下来要列出 m m m 个关系。

接下来 m m m 行,每行一个字符 o p t opt opt 和两个整数 p , q p,q p,q,分别代表关系(朋友或敌人),有关系的两个人之中的第一个人和第二个人。其中 o p t opt opt 有两种可能:

  • 如果 o p t opt optF,则表明 p p p q q q 是朋友。
  • 如果 o p t opt optE,则表明 p p p q q q 是敌人。

【输出格式】

一行一个整数代表最多的团体数。

【示例一】

输入

6
4
E 1 4
F 3 5
F 4 6
E 1 2

输出

3

【说明/提示】

对于 100 % 100\% 100% 的数据, 2 ≤ n ≤ 1000 2 \le n \le 1000 2n1000 1 ≤ m ≤ 5000 1 \le m \le 5000 1m5000 1 ≤ p , q ≤ n 1 \le p,q \le n 1p,qn


(1) 解题思路

开辟一个 2 n + 10 2n + 10 2n+10 大小的数组, [ 1 , n ] [1, n] [1,n] 区间表示朋友域, [ n + 1 , 2 n ] [n + 1, 2n] [n+1,2n]​ 区间表示敌人域,如果 x x x y y y 是朋友,那么把 x x x y y y 合并成一个集合;如果 x x x y y y 是敌人,那么我们把 x x x y + n y + n y+n 合并成一个集合, x + n x + n x+n y y y 合并成一个集合。

处理完之后我们只需看有多少个根节点即可。


(2) 代码实现

#include<iostream>

using namespace std;

const int N = 1010;
int pa[2 * N];  // 扩展域并查集, 两种关系,扩展出两个域
int n, m;

void init()
{
    for(int i = 1; i <= 2 * n; i++) pa[i] = i;
}

int find(int x)
{
    if(pa[x] == x) return x;
    return pa[x] = find(pa[x]);
}

// 细节:由于最后我们看有多少个根节点的时候只遍历朋友域
// 因此我们合并的时候要让朋友域中的节点作为父亲节点合并
void uni(int x, int y)
{
    // 这段逻辑中 fy 是父亲
    int fx = find(x);
    int fy = find(y);
    pa[fx] = fy;
}

int main()
{
    cin >> n >> m;
    init();
    while(m--)
    {
        char opt; cin >> opt;
        int p, q; cin >> p >> q;
        if(opt == 'F') uni(p, q);
        else
        {
            // 注意:让朋友域作为父亲合并
            uni(p + n, q);
            uni(q + n, p);
        }
    }
    int cnt = 0;
    for(int i = 1; i <= n; i++)
    {
        if(pa[i] == i) cnt++;
    }
    cout << cnt << endl;

    return 0;
}

2. 食物链 ⭐⭐⭐

【题目链接】

[P2024 NOI2001] 食物链 - 洛谷

【题目描述】

动物王国中有三类动物 A , B , C A,B,C A,B,C,这三类动物的食物链构成了有趣的环形。 A A A B B B B B B C C C C C C A A A

现有 N N N 个动物,以 1 ∼ N 1 \sim N 1N 编号。每个动物都是 A , B , C A,B,C A,B,C 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N N N 个动物所构成的食物链关系进行描述:

  • 第一种说法是 1 X Y,表示 X X X Y Y Y 是同类。
  • 第二种说法是 2 X Y,表示 X X X Y Y Y

此人对 N N N 个动物,用上述两种说法,一句接一句地说出 K K K 句话,这 K K K 句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  • 当前的话与前面的某些真的话冲突,就是假话;
  • 当前的话中 X X X Y Y Y N N N 大,就是假话;
  • 当前的话表示 X X X X X X,就是假话。

你的任务是根据给定的 N N N K K K 句话,输出假话的总数。

【输入格式】

第一行两个整数, N , K N,K N,K,表示有 N N N 个动物, K K K 句话。

第二行开始每行一句话。格式见题目描述与样例。

【输出格式】

一行,一个整数,表示假话的总数。

【示例一】

输入

100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5

输出

3

【说明/提示】

对于全部数据, 1 ≤ N ≤ 5 × 1 0 4 1\le N\le 5 \times 10^4 1N5×104 1 ≤ K ≤ 1 0 5 1\le K \le 10^5 1K105


(1) 解题思路

通过题目描述,我们可以得出三类动物之间一共有 3 3 3 种关系:捕食、被捕食、同类

所以想要维护它们之间的关系可以采用扩展域并查集,扩展出来 3 3 3 个域,需要开辟一个 3 n 3n 3n 的数组。针对某一个动物 x x x 1 ≤ x ≤ n 1\le x\le n 1xn),扩展出 3 3 3 个域:同类域 [ 1 , n ] [1, n] [1,n],捕食域 [ n + 1 , 2 n ] [n + 1, 2n] [n+1,2n],被捕食域 [ 2 n + 1 , 3 n ] [2n + 1, 3n] [2n+1,3n]

如果 x x x y y y 是同类,那么:

  • x x x y y y 是同类,合并 x x x y y y
  • x x x y y y 所捕食的物种是同类,合并 x + n x + n x+n y + n y + n y+n
  • 捕食 x x x y y y 的物种是同类,合并 x + 2 n x + 2n x+2n y + 2 n y + 2n y+2n

如果 x x x 捕食 y y y,那么:

  • x x x 捕食 y y y,合并 x x x y + n y + n y+n
  • x x x 捕食的物种和捕食 y y y 的物种是一类,合并 x + n x + n x+n y + 2 n y + 2n y+2n
  • 捕食 x x x 的物种和 y y y 捕食的物种是一类,合并 x + 2 n x + 2n x+2n y + n y + n y+n

(2) 代码实现

#include<iostream>

using namespace std;

const int N = 5e4 + 10;
int pa[3 * N];
int n, k;

void init()
{
    for(int i = 1; i <= 3 * n; i++) pa[i] = i;
}

int find(int x)
{
    if(pa[x] == x) return x;
    return pa[x] = find(pa[x]);
}

void uni(int x, int y)
{
    int fx = find(x);
    int fy = find(y);
    pa[fx] = fy;
}

bool issame(int x, int y)
{
    return find(x) == find(y);
}

int main()
{
    scanf("%d%d", &n, &k);
    int cnt = 0;  // 记录假话个数
    init();
    while(k--)
    {
        int r, x, y;
        scanf("%d%d%d", &r, &x, &y);
        // 如果有一个比 n 大或者 x 吃 x
        if((x > n || y > n) || (r == 2 && x == y))
        {
            cnt++;
            continue;
        }

        // x, y 是同类关系
        if(r == 1)
        {
            // 如果发现 x 如吃 y 或者 x 被 y 吃
            bool t = issame(x, y + n) || issame(x, y + 2 * n);
            if(t)  // 那么就矛盾了,说明是假话
            {
                cnt++;
                continue;
            }
            // 否则就是真话
            uni(x, y);
            uni(x + n, y + n);
            uni(x + 2 * n, y + 2 * n);
        }
        // x 吃 y
        else
        {
            // 此时如果发现 x, y 是同类或者 x 被 y 吃
            bool t = issame(x, y) || issame(x, y + 2 * n);
            if(t)  // 那么就矛盾了,说明是假话
            {
                cnt++;
                continue;
            }
            // 否则就是真话
            uni(y + n, x);
            uni(x + n, y + 2 * n);
            uni(x + 2 * n, y);
        }
    }
    cout << cnt << endl;

    return 0;
}
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值