poj_2985 The k-th Largest Group 树状数组求第K大

题目链接:http://poj.org/problem?id=2985

题目大意:某人养了很多猫,他会把一些猫合并成一组,并且会询问第k大组有几只猫

前面说了树状数组的三个用途,现在说一下树状数组求  第K大。

对于一个数组,求第K大的数,只需要sort排序一下,取第k个值即可,但是当不停的改变数组中元素时(即在线询问),对于每次询问的第k大值,都要对数组重新进行排序,时间复杂度(快排N*logN)*(询问N);

那我们来看一下用树状数组:

首先树状数组C【i】里面存的是在i管辖的范围内各个组数的和,比如  1出现2次,2出现3次,4出现6次,那么a【1】=2,a【2】=3,a【3】=0,a【4】=6;故c【4】=11;

对于a数组{1,1,2,2,2,2,3,4,5,5,5,5,6,7,8};

则现在的a数组为{2,4,1,1,4,1,1,1},有这个数组,树状数组应该自己能构建出来了吧。。。

第一种方法:二分法

每次二分产生一个mid,判断get_sum(mid)的值与k的大小,更新mid的值,代码如下:

#include<stdio.h>
#define N 200005
int m,n;
int f[N],a[N],c[N];
void init()//初始化
{
    for(int i=1;i<N;i++)
    {
        f[i]=i;
        a[i]=1;//初始状态每种编号的猫只有一只
    }
}
int find(int x)//判断两堆猫是否在一个集合
{
   if(x==f[x]) return x;
   return f[x]=find(f[x]);
}
int lowbit(int x)  {return x&(-x);}
void insert(int x,int v)//更改某种数量猫的个数
{
    while(x<N)
    {
        c[x]+=v;
        x+=lowbit(x);
    }
}
int get_sum(int x)//求和
{
    int sum=0;
    while(x>0)
    {
        sum+=c[x];
        x-=lowbit(x);
    }
    return sum;
}
int main()
{
    int m,n;
    while(~scanf("%d%d",&n,&m))
    {
        init();
        insert(1,n);//个数为1的堆数有N堆,故在1处插入N,向后更新
        int num=n;
        for(int i=1;i<=m;i++)
        {
            int mark;
            scanf("%d",&mark);
            if(mark==0)
            {
                int x,y;
                scanf("%d%d",&x,&y);
                int fx=find(x);
                int fy=find(y);
                if(fx==fy) continue;
                insert(a[fx],-1);//当fx和fy合并后,数量为fx和fy的堆数都减少1
                insert(a[fy],-1);
                a[fy]+=a[fx];
                insert(a[fy],1);//数量为fx+fy的堆数增加1
                f[fx]=fy;
                num--;//没次合并总堆数-1
            }
            else
            {
                int k;
                scanf("%d",&k);
                k=num-k+1;//第k大即第num-k+1小
                int s=1,e=n,mid;
                while(s<=e)
                {
                    mid=(s+e)/2;
                    if(get_sum(mid)>=k)//尽量向左逼近
                        e=mid-1;
                    else
                        s=mid+1;
                }
                printf("%d\n",s);
            }
        }
    }
    return 0;
}

上面的方法还比较好理解,下面说一下第二种方法:

首先说一个东西:任何一个正整数都能分成几个数的平方的和,然后就有了下面的方法:

前k大的数我们假如有x个,那么我们就先找到一个比x小的最大的数y,然后x=x-y,然后再找一个y,不停的找下去,直到x为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的个数
简言之:上述的各个数字两两间控制的范围不会相交

代码如下:

#include<stdio.h>
#define N 200005
int m,n;
int f[N],a[N],c[N];
void init()
{
    for(int i=1;i<N;i++)
    {
        f[i]=i;
        a[i]=1;
    }
}
int find(int x)
{
   if(x==f[x]) return x;
   return f[x]=find(f[x]);
}
int lowbit(int x)  {return x&(-x);}
void insert(int x,int v)
{
    while(x<N)
    {
        c[x]+=v;
        x+=lowbit(x);
    }
}
int get_sum(int x)
{
    int sum=0;
    while(x>0)
    {
        sum+=c[x];
        x-=lowbit(x);
    }
    return sum;
}
int find_kth(int k)
{
    int ans=0,cnt=0;
    for(int i=20;i>=0;i--)//1<<20应该不小了吧
    {
        ans+=(1<<i);
        if(ans>N||cnt+c[ans]>=k)
            //这里大于等于k的原因是可能大小为ans的数不在c[ans]的控制范围之内,所以这里求的是 < k
            ans-=(1<<i);
        else
          //cnt用来累加比当前ans小的总组数
            cnt+=c[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 m,n;
    while(~scanf("%d%d",&n,&m))
    {
        init();
        insert(1,n);
        int num=n;
        for(int i=1;i<=m;i++)
        {
            int mark;
            scanf("%d",&mark);
            if(mark==0)
            {
                int x,y;
                scanf("%d%d",&x,&y);
                int fx=find(x);
                int fy=find(y);
                if(fx==fy) continue;
                insert(a[fx],-1);
                insert(a[fy],-1);
                a[fy]+=a[fx];
                insert(a[fy],1);
                f[fx]=fy;
                num--;
            }
            else
            {
                int k;
                scanf("%d",&k);
                k=num-k+1;
                printf("%d\n",find_kth(k));
            }
        }
    }
    return 0;
}

品味。。。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值