"科林明伦杯"哈尔滨理工大学第八届程序设计竞赛——Hrbust -2371 GT’s Dream(并查集+树状数组+二分)

Description
在现实中认了无数师傅却毫无长进的GT在梦中成为了某武侠世界的神。在这个世界中初始有n个人,他们各成一派。作为世界神GT总共会进行m次操作,每次操作有如下两种情况

1 x y 表示x所在的帮派吞并了y所在的帮派,若x与y本来就处于同一个帮派则该操作无效。

2 k 表示GT想要知道当前第k大的帮派有多少人,若当前帮派数量少于k个则输出-1。

Input

第一行输入一个T(T<=10)表示有T组测试数据。

每组数据的第一行包含两个数n,m(n<=100000,m<=300000)表示有n个人,m次操作

接下来m行包含每一次的操作情况。

Output
对于每一个2操作输出当前第k大的帮派有多少人。

Sample Input
1

5 4

1 2 4

1 3 4

2 1

2 2

Sample Output
3

1

题意:有n个帮派,每个帮派1人,有m次操作,可以选择1操作让x帮派吞并y帮派,或2操作查询第k大帮派的人数。若K大于实际存在的帮派数量则输出-1

题解: 首先根据题意帮派的吞并很显然是用并查集实现,加一个数组存储当前帮派的人数即可。
而查询第k大帮派的人数就要一个前缀和数组来实现,前缀和数组中下标表示人数,每个位置表示该人数的出现次数。也就是有多少个帮派有相同的人数。那么可以发现,此数组的前缀和,即达到某一个人数时的帮派数量总和,下标即是此总和的人数,那么这个总和的下标就是第k小的帮派人数,因为下标是递增顺序增长的,也就是说或要达到这个帮派数k,说明之前有比这个帮派人少的帮派,并且至少有k-1个,那么我们可以求得每个下标的总和,即可以拿前缀和和k比较,若前缀和大于k则说明我们找的下标大了,若小于k说明当前找的的下标并不是第k个小的帮派人数,这样二分查找直到找到一个前缀和等于k的,输出下标即结果。

至于题目要找的是第k大的,那么我们即可拿总帮派数-k+1得到数组中第k大的应该查找的帮派数量。

关于几点要注意的,并查集更新帮派时,对某个帮派的人数要初始为0,另一个要加上被吞并的帮派人数。而树状数组中的前缀和则要更新两个帮派原人数的计数,都减去1,并在新人数的计数+1,注意初始化时不仅要初始化并查集数组,还要对每个帮派的人数初始化为1。
对于真实帮派数real在确定吞并时才修改,有时修改是无效的。

对于求解k时也是用真实帮派数量。而二分时用的则是N,二分区间是不会改变的,因为总人数不会减少。

#include<stdio.h>
#include<string.h>
int z[100004],tree[100005],num[100005];
int n,t,m,real;
int lowbit(int x)///树状数组用于更新不同人数出现的帮派数的前缀和
{
    return x&(-x);
}
int update(int p,int x)
{
    while(p<=n)
    {
        tree[p]+=x;
        p+=lowbit(p);
    }
}
int sum(int p)
{
    int sum=0;
    while(p>0)
    {
        sum+=tree[p];
        p-=lowbit(p);
    }
    return sum;
}
int finds(int x)
{
    int tmp;
    while(z[x]!=x)
    {
        tmp=x;
        x=z[x];
        z[tmp]=z[x];
    }
    return x;
}
void join(int a,int b)
{
    int fa=finds(a),fb=finds(b);
    if(fa!=fb)
    {
        z[fb]=fa;
        update(num[fa],-1);///当帮派合并,两个帮派的人数都被改变,那么该人数存在的帮派数量也要相应改变
        update(num[fb],-1);
        num[fa]+=num[fb];///表示a吞并b
        num[fb]=0;///b帮派重置为0人
        update(num[fa],1);///新的a帮派人数,将增加一个帮派数量
        real--;///注意真实帮派数的减少,当连接两帮派时不一定减少,因为还有可能是无效操作,确定减少了再减去这个 真实值
    }
}
int main()
{
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++) num[i]=1,z[i]=i;
        memset(tree,0,sizeof(tree));
        update(1,n);
        real=n;
        while(m--)
        {
            int flag;
            scanf("%d",&flag);
            if(flag==1)
            {
                int x,y;
                scanf("%d%d",&x,&y);
                join(x,y);
            }
            else
            {
                int k;
                scanf("%d",&k);
                if(k>real)
                {
                    printf("-1
");
                    continue;
                }
                k=real-k+1;///树状数组前缀和计算的是第k小人数,那么我要计算的第k大人数,应是实际帮派数-k+1(本身)的位置
                int l=1,r=n;///要保留原人数的原因,因为二分时做一个最大人数的最大边界,不能因为帮派数量减少就减少搜索区间
                while(l<r)///根据k值二分查找前缀和中第一个大于等于k的位置,也就是该帮派的人数
                {
                    int mid=(l+r)/2;
                    if(sum(mid)>=k) r=mid;
                    else l=mid+1;
                }
                printf("%d
",l);
            }
        }

    }
}
///树状数组中存储的是当人数为i时的帮派数量,也是人数的出现次数
///如果求第k小人数的帮派人数。那么就是说该人数的前缀和,就包括了所有小于等于该人数的帮派数量,当前缀和大于等于k时,说明该人数之前已经有小于k个帮派,那么数量k的帮派位于的下标即第k小帮派人数
///若前缀和,从1到某值的下标,表示不同人数出现的次数,那么前缀和为k表示了在此之前有k-1个帮派存在,那么第k个帮派的人数就是下标
///求第k大帮派人数,即当前存在的总帮派数-k值+1得到在树状数组中求第新的k的人数,也就是下标

原文地址:https://www.cnblogs.com/kuronekonano/p/11135812.html