洛谷 1196 题解

题意简述

300003000030000个队列,初始每个队列里有元素1,2,3⋯300001,2,3\cdots 300001,2,330000。两种指令:
1.MMM iii jjj 编号为iii的队列整个接到jjj
2.CCC iii jjj 问编号为iii,jjj的两个点之间(不包含iii,jjj)有多少点。(如果不在一个队列里输出−1-11

数据

输入
4
M 2 3
C 1 2
M 2 4
C 4 2
输出
-1
1

思路

这个题目是我在体育课上完成的思路,回来写的代码。。。
趁着刚做完,讲讲我思考的过程。
刚看到这个题的时候,第一个想到的肯定是并查集。但是突然注意到询问是要求中间有多少点,一脸懵逼:这怎么维护?难道是上Splay??(珂是Splay我不会啊。。。)

慢慢分析这个题,我们肯定不能暴力维护点的位置。想想那些区间求和的问题,都怎么做的:没错,前缀和\color{red}前缀和!我们用Sum[i]Sum[i]Sum[i]表示iii这个点在iii的队列中前面(不包括iii)有多少点。在询问的时候,只要将两个SumSumSum值取绝对值相减,然后±1\pm1±1或者不变就珂以得到了。究竟是哪个呢?经过对样例的模拟,我们会发现此时应该是−1-11才是正确的。

上面这个看起来很好理解。那么如果我们要合并两个队列的时候,比方说是xxx队列和yyy队列,那么xxx队列的每个元素的sumsumsum都因为接在了yyy队列的后面,所以每个都要+=Cnt[y]+=Cnt[y]+=Cnt[y],其中Cnt[y]Cnt[y]Cnt[y]表示yyy队列的元素个数。由于我们需要修改所有xxx队列中的元素,遍历就不珂避免了,一次合并操作就要O(n)O(n)O(n)了。怎么优化这个呢?

我们想想我们在搞并查集问题的时候都是怎么搞的:一个祖先一一对应一个集合。一个集合中所有的记录工作都交给祖先来完成\color{red}交给祖先来完成。所以我们考虑这样做:只对祖先的SumSumSum加值,在查找点iii的时候逐个加上父亲(不包括祖先)的SumSumSum值,就是点iii实际的SumSumSum值。然后发现查找就O(n)O(n)O(n)了,因为我们如果进行路径压缩,信息就丢失了。而实际上,我们珂以在路径压缩的时候就更新i点的Sum\color{red}路径压缩的时候就更新i点的SumiSum值,然后让iii就算直接接到祖先上,也不会丢失数据,就保证了信息的正确性。这很好办,只要在路径压缩回溯的路上顺便加上这个值即可。

这样一次操作就O(1)O(1)O(1)了!!!

代码:

#include<bits/stdc++.h>
#define N 1001000
using namespace std;
class DSU
{
    private:
        int Father[N];
    public:
        int Cnt[N],Sum[N];
        //和上面说的一样.Cnt记录集合大小,Sum记录前面多少点
        //注意这个"前面"是不包括i本身的
        void Init()
        {
            for(int i=1;i<N;i++)
            {
                Father[i]=i;
                Cnt[i]=1;
                Sum[i]=0;//所以这边是0
            }
        }
        int Find(int x)
        //找到x的祖先,并将x的Sum值设置成所有父亲(当然,不包括祖先)
        {
            if (Father[x]==x) return x;//不包括祖先,所以此时直接返回x

            int ax=Find(Father[x]);
            Sum[x]+=Sum[Father[x]];//Father[x]在经过一轮Find后,Sum[Father[x]]已经设置成了Father[x]所有父亲(不包括祖先)的Sum和。然后只要用这一行代码,就珂以将Sum[x]的值设置成x所有父亲(不包括祖先)的Sum和了。

            return Father[x]=ax;//此时已经记录好了,珂以路径压缩了(准确来讲,此时如果不路径压缩会得到错误答案。仔细想想)
        }
        void Merge(int x,int y)//队列x接到队列y
        {
            int ax=Find(x),ay=Find(y);

            Sum[ax]+=Cnt[ay];//记录好Sum(只给祖先加上)
            Cnt[ay]+=Cnt[ax];//Cnt也要记录好
            Father[ax]=ay;//祖先接过去
        }
}D;

void Query()
{
    int T;scanf("%d",&T);
    while(T--)
    {
        char o[3];int x,y;
        scanf("%s%d%d",o,&x,&y);
        if (o[0]=='M')
        {
            D.Merge(x,y);
        }
        else
        {
            int ax=D.Find(x),ay=D.Find(y);
            if (ax!=ay)
            //不在一个队列里
            {
                printf("-1\n");
            }
            else
            //在一个队列里
            {
                printf("%d\n",abs(D.Sum[x]-D.Sum[y])-1);
            }
        }
    }
}

main()
{
    D.Init();
    Query();
    return 0;
}
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值