POJ2985_The k-th Largest Group(树状数组)

本文介绍了一种使用树状数组求解第K大元素的高效算法,通过将问题转化为求第K小元素,利用树状数组的特性进行快速查询。文章详细解释了树状数组的工作原理,包括如何通过二进制思想逆向求和,找到满足条件的元素。并通过具体代码展示了整个算法的实现过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转自:https://www.cnblogs.com/wuyiqi/archive/2011/12/25/2301071.html

题意:

首先给你N只老鼠,M个操作;最开始一只老鼠一个组;

输入0,输入两个数i,j;使得i老鼠与j老鼠的组合并;

输入1,输入一个数K,求第K大的组有多少只老鼠。

思路:

第K大就是第n-k+1小,用树状数组求第K小。
觉得:https://www.cnblogs.com/wuyiqi/archive/2011/12/25/2301071.html
讲的很好,下面摘录一些。
树状数组经典图片:
在这里插入图片描述
现在假设要求sum[a]的值,一般我们都是从后往前求和,如a=15

15-lowbit(15)=14;

14-lowbit(14)=12;

12-lowbit(12)=8;

8-lowbit(b)=0;

答案就是sum[15]+sum[14]+sum[12]+sum[8];

现在我们可以这样来求,从不超过15的只有一个1的最大二进制数开始,也可以理解为指数从log(15)取整开始,即3,2的3次等于8,依次加上2的2次,2的1次,2的0次,数字依次为8,12,14,15,也就是把普通的求和过程反向。

好了,方向求和有什么好处呢?

在求第k大的数的时候就派上用场了,虽然还有很多其他方法可以解决第k大的数,但树状数组无疑是最优雅的方法了

int find_kth(int k)//太神奇了(大概是以前没有完全领会),log(n)复杂度
{
 int ans = 0, cnt = 0, i;
 for (i = 20; i >= 0; i--)//利用二进制的思想,把答案用一个二进制数来表示
 {
	 ans += (1 << i);
	 if (ans >= maxn|| cnt + c[ans] >= k)
	 //这里大于等于k的原因是可能会有很多个数都满足cnt + c[ans] >= k,所以找的是最大的满足cnt+c[ans]<k的ans
	 	ans -= (1 << i);
	 else
		 cnt += c[ans];//cnt用来累加比当前ans小的总组数
	 }//求出的ans是累加和(即小于等于ans的数的个数)小于k的情况下ans的最大值,所以ans+1就是第k大的数
	 return ans + 1;
}
完整代码:
#include<stdio.h>
#include<string.h>
#define maxn 300000
int a[maxn],c[maxn],p[maxn];//值为i的数有i个
int find(int x){return x==p[x] ? x : p[x]=find(p[x]);}
int lowbit(int x){
    return x&-x;
}
void update(int x,int d){
    for(;x<=maxn;x+=lowbit(x))
        c[x]+=d;
}//因为是从左往右手动求和了,所以也不需要sum()操作了
int find_kth(int k)//太神奇了(大概是以前没有完全领会),log(n)复杂度
{
    int ans = 0, cnt = 0, i;
    for (i = 20; i >= 0; i--)//利用二进制的思想,把答案用一个二进制数来表示
    {
        ans += (1 << i);
        if (ans >= maxn|| cnt + c[ans] >= k)
            //这里大于等于k的原因是可能大小为ans的数不在c[ans]的控制范围之内,所以这里求的是 < k
            ans -= (1 << i);
        else
            cnt += c[ans];//cnt用来累加比当前ans小的总组数
    }//求出的ans是累加和(即小于等于ans的数的个数)小于k的情况下ans的最大值,所以ans+1就是第k大的数
    return ans + 1;
}
/*
因为要求第k小的数,所以要从左往右加过去,
上述过程其实就是把树状数组的求和操作逆向,从左往右求和,
边求和边判断控制范围内比当前值要小的数是否超过或等于k,如果是则跳回兄弟节点(ans-=(1<<i))
如8+4=12,假如12不满足要求,则重新变回8,下一次就加2,8+2=10,即跳到10控制的位置
上述累加过程不会重复计算,因为
比如15=8+4+2+1,数字依次为8  12  14  15,每次累加后的值都与前面的值无关,i小于其二进制末尾0的个数
即c[8] 、c[12] 、c[14]、 c[15]相加的话不会重复计算,再如11=8+2+1;数字依次为8 10 11,c[8],c[10],c[11]
各自控制着自己的范围,不会重复累加,所以就可以用cnt来累加前面的结果,最后cnt+c[ans]就表示了值<=ans的个数
简言之:上述的各个数字两两间控制的范围不会相交
*/
int main()
{
    int i,n,m,q,x,y,k,l,r;
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;i++) p[i]=i;
    for(i=1;i<=n;i++) a[i]=1;
    update(1,n);//初始状态值为1的数有n个
    int num=n;
    for(i=1;i<=m;i++)
    {
        scanf("%d",&q);
        if(q==0)
        {
            scanf("%d%d",&x,&y);
            x=find(x);
            y=find(y);
            if(x==y) continue;
            update(a[x],-1);
            update(a[y],-1);
            update(a[x]+a[y],1);
            p[y]=x;
            a[x]+=a[y];
            num--;//合并集合
        }
        else 
        {
            scanf("%d",&k);
            k=num-k+1;//转换为找第k小的数
            printf("%d\n",find_kth(k));
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值