并查集的应用 方法1.维护节点 到 根节点的距离 方法2:扩展与并查集: 奇偶游戏

题目链接:https://www.acwing.com/problem/content/description/241/

题目:

小 A 和小 B 在玩一个游戏。

首先,小 A 写了一个由 0 和 1 组成的序列 S,长度为 N。

然后,小 B 向小 A 提出了 M 个问题。

在每个问题中,小 B 指定两个数 l 和 r,小 A 回答 S[l∼r] 中有奇数个 1 还是偶数个 1。

机智的小 B 发现小 A 有可能在撒谎。

例如,小 A 曾经回答过 S[1∼3] 中有奇数个 1,S[4∼6] 中有偶数个 1,现在又回答 S[1∼6] 中有偶数个 1,显然这是自相矛盾的。

请你帮助小 B 检查这 M 个答案,并指出在至少多少个回答之后可以确定小 A 一定在撒谎。

即求出一个最小的 k,使得 01 序列 S 满足第 1∼k个回答,但不满足第 1∼k+1 个回答。

输入格式

第一行包含一个整数 N,表示 01 序列长度。

第二行包含一个整数 M,表示问题数量。

接下来 M 行,每行包含一组问答:两个整数 l 和 r,以及回答 even 或 odd,用以描述 S[l∼r]中有偶数个 1还是奇数个 1。

输出格式

输出一个整数 k,表示 01 序列满足第 1∼k个回答,但不满足第 1∼k+1 个回答,如果 01 序列满足所有回答,则输出问题总数量。

数据范围

N≤1e9,M≤10000

输入样例:

10
5
1 2 even
3 4 odd
5 6 even
1 6 even
7 10 odd

输出样例:

3

方式1:并查集维护节点到根节点的距离,从而判断是否为同一类 (与之前并查集之 “食物链”的题目类似)

首先根据题目可以发现,N很大,1e9。 但是所用到的节点数为 2 * M , 2e4左右 

所以可以想到与前面的并查集的应用之 "程序自动分析"一样 要用到离散化。

链接:https://blog.csdn.net/qq_49120553/article/details/118760820?spm=1001.2014.3001.5501

同时可以发现此题的离散化 与 “程序自动分析”一样,不需要考虑离散化的值的顺序。

只是判断离散化的值 x 与 p[x]是否相同来判断是否为根节点,或者p[x]与p[y]是否在同一集合等等操作。  所以与顺序无关,使用哈希unordered_map  进行离散化即可。

而怎么使用并查集呢?

输入中会给出l,r 之间1的个数是奇数还是偶数。并且字符串只有1和0两部分组成。

那么我们可以使用前缀和的思想,如 l~r之间1的个数为奇数的话,

则 s[r] - s[l - 1]为奇数。  而这个的含义就是s[r] 与 s[l-1]一个为奇数一个为偶数。

同理 ,l~r之间1的个数为偶数的话

则s[r] - s[l - 1]为偶数,而这个的含义就是s[r] 与s[l - 1]要么两个都为奇数,要么两个都为偶数。

所以,也就可以通过奇偶性,如 l ~ r为奇数, 则s[r]与s[l - 1]不同类, l ~ r为偶数,则s[r]与 s[l - 1]同类 进行判断。

但是,这样的猜想是否正确呢?奇数个1,则两个不同类,偶数个1,则两个为同一类。

是正确的。因为我们可以发现,题目中并没有给出具体的字符串的形式。 所以只要你给出一个s[i]为奇数,或者偶数, s[ i - 1]为一个奇数 或者 偶数, 就可以构造出一种方式使得其成立

如: s[i]为奇数, s[i - 1]为偶数,也就是前 i - 1个中有偶数个1, 前i个中有奇数个1, 所以可以根据s[i],s[i - 1]的情况构造出一个a[i] 为 1 , 使得其成立

同样,s[i]为奇数,s[ i - 1]为奇数, 则a[i]为0。

所以,给出一个s[r], s[l]就一定至少一种方式可以构造出一个满足此s[r],s[l]的字符串出来。

所以此题就是用来判断,s[r] 与 s[l - 1]  是否为同类, 如前面推出s[r] 与 s[l - 1]为同类, 后面推出  s[r] 与 s[l - 1]不同类 ,则矛盾。 

 而判断两个节点是否同类就可以使用并查集。

分析:并查集 维护 节点到根节点的使用(与食物链题目类似)

应该怎么维护 根节点 到 节点 之间的距离呢?使用d[i],进行维护。 

其中d[i]的含义是i节点到根节点之间的距离。

情况如下:

1.如果题目给出l~r中有偶数个1

则s[r] , s[l - 1]两者同类。

(1)r,l - 1在同一集合中,则d[r] + d[l - 1]为偶数即可。

分析:因为两个数相加,奇数 + 奇数 或者 偶数 + 偶数 才能为偶数

(2)不在同一集合中, 则将 l - 1的集合 并到  r的集合中去

 

 所以d[p[l- 1]] = d[r] + d[l - 1]

2.如果题目给出l~r中有奇数个1

则s[r] , s[l - 1]两者不同类。

(1)r,l - 1在同一集合中,则d[r] + d[l - 1]为奇数即可。

分析:因为两个数相加,偶数 + 奇数 才为奇数

(2)不在同一集合中, 则将 l - 1的集合 并到  r的集合中去

 所以d[p[l- 1]] = d[r] + d[l - 1] - 1即可

代码:

# include <iostream>
# include <string>
# include <unordered_map>
using namespace std;
const int N = 20010;

int p[N];
int d[N];  // d[i]为 i节点 到 父结点 的距离。
unordered_map<int,int> h;
int idx;

int n,m;

int get(int x)
{
    if(!h.count(x))
    {
        h[x] = ++idx;
    }
    return h[x];
}

int find2(int x)
{
    if(p[x] != x)
    {
        int t = p[x];
        p[x] = find2(p[x]);
        d[x] += d[t];
    }
    return p[x];
}

int main()
{
    scanf("%d %d",&n,&m);
    for(int i = 1 ; i <= 2 * m ; i++)
    {
        p[i] = i;
        d[i] = 0;  
    }
    
    int cnt = m;
    for(int i = 1 ; i <= m ; i++)
    {
        int a,b;
        string ch;
        cin >> a >> b >> ch;
        
        int x = get(a - 1); // 因为l~r中1的个数,则s[r] - s[l - 1]为奇数
        int y = get(b);
        
        int px = find2(x);
        int py = find2(y);
        if(ch == "even")  // 偶数
        {
            if(p[px] == p[py])
            {
                if( (d[x] + d[y]) % 2 != 0) // 不为偶数
                {
                    cnt = i - 1;
                    break;
                }
            }
            else  //让x集合 接到 y的集合上
            {
                d[px] = d[x] + d[y];
                p[px] = p[py];
            }
        }
        else
        {
            if(p[px] == p[py])
            {
                if( (d[x] + d[y]) % 2 == 0)
                {
                    cnt = i - 1;
                    break;
                }
            }
            else
            {
                d[px] = d[x] + d[y] - 1; 
                p[px] = p[py];
            }
        }
    }
    printf("%d\n",cnt);
    return 0;
}

 方式2:扩展域并查集

扩展域并查集有一个核心的思路就是:合并多个条件,如果某个条件在这个集合中成立,则这个集合中的其他条件必须全部成立, 如果某个条件在这个集合中不成立, 则这个集合中的其他条件是否成立则无所谓。

而方式1则主要是合并两个节点,两个节点之间的关系则需要通过距离进行判断。

用不同的数值代表状态。r代表s[r]为偶数的情况,r + m 为 s[r]为奇数的情况 

所以如:

(1)s[r] 与 s[l - 1] 是同类,

则合并r 与 l - 1. 并且合并 r + m , l - 1 + m. 

即 合并s[r]为偶数,s[l - 1]也为偶数的情况,  s[r]为奇数,s[l - 1]为奇数的情况。

(2) s[r] 与 s[l - 1]不是同类

则合并 r 与 l - 1 + m,  并且合并 r + m 与 l - 1

即 合并s[r]为偶数,s[l - 1]为奇数的情况,  s[r]为奇数,s[l - 1]为偶数的情况。

这样合并后如何判断所给条件是否矛盾呢?

就用到了,集合中某个条件成立,则其他条件必须成立,  集合中某个条件不成立,则其他条件无所谓的性质。

 s[r] 与 s[l - 1] 是同类

则说明 r,l - 1 在一个集合中,  r + m , l - 1 + m 在一个集合中,

于是从中任意判断一个:如 p[r] 与 p[l - 1 + m] 是否在同一集合中。

(1)如果在同一集合中,则 r + m , 与 l - 1 + m 不在同一集合中,发生矛盾。

则说明 r 与 l - 1不是同类, 条件有矛盾。

(2)如果r 与 l - 1 + m  不在同一集合中, 则其他条件无所谓,因为其他的条件也一定不在同一集合中, 无法进行判断, 所以进行赋值,让它们在同一集合中即可,用于判断后面条件与当下这个条件以后是否会矛盾。

 代码实现:

# include <iostream>
# include <unordered_map>
using namespace std;
const int N = 40010;

unordered_map<int,int> h;
int idx;

int p[N];

int get(int x)
{
    if(h.count(x) == 0)
    {
        h[x] = ++idx;
    }
    return h[x];
}

int find2(int x)
{
    if(x != p[x])
    {
        p[x] = find2(p[x]);
    }
    return p[x];
}

// x,y分别代表偶数, x + n , y + n 代表x,y分别为奇数

int main()
{
    int n,m;
    scanf("%d %d",&n,&m);
    for(int i = 1 ; i <= 4 * m ; i++)  //所有的l,r都会被离散化到M 以内,l和 l + m , r 和 r + m 共4种值,所以开4倍
    {
        p[i] = i;
    }
    int cnt = m;
    for(int i =1 ; i <= m ; i++)
    {
        int l ,r;
        string ch;
        cin >> l >> r >> ch;
        int x = get(l - 1);
        int y = get(r);
        if(ch == "even") //偶数
        {
            if(find2(x) == find2(y + m))
            {
                cnt = i - 1;
                break;
            }
            else // 实际不用加else,但加else更容易让我自己看懂其中逻辑
            {
                p[find2(x)] = p[find2(y)];
                p[find2(x + m)] = p[find2(y + m)];
            }
        }
        else  // 奇数,不同类
        {
            if(find2(x) == find2(y)) // 矛盾
            {
                cnt= i - 1;
                break;
            }
            else
            {
                p[find2(x + m)] = p[find2(y)];
                p[find2(x)] = p[find2(y + m)];
            }
        }
    }
    printf("%d\n",cnt);
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值