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的人数,也就是下标