POJ 2104 K-th Number

题目大意:

Given an array a[1...n] of different integer numbers, your program must answer a series of questions Q(i, j, k) in the form: "What would be the k-th number in a[i...j] segment, if this segment was sorted?"

区间长度和询问次数都是10^5数量级,数字大小<10^9,显然需要寻求一种O(MlogN)的算法实现,这里给出主席树和划分树两种解法。

方法一:主席树

将区间元素排序,将排序后的id对应原区间位置的数进行离散化。然后将大小顺序作为持久维度,从小到大在线段树中动态插入结点,保存每次插入后的线段树版本。以后每次询问区间[L,R]第K大数时,只要处理tree[L-1]和tree[R]两棵之间的结点域sum值之差即可知道[L,R]内有多少元素在这个线段树节点内。

Source Code
Problem: 2104		User: xushu
Memory: 22620K		Time: 1516MS
Language: G++		Result: Accepted

    #include <iostream>
    #include <cstdio>
    #include <vector>
    #include <algorithm>
    using namespace std;
    const int maxn=1e5+5;
    struct node {int l,r,sum;} t[maxn*30];
    int root[maxn],a[maxn],n,m,cnt=0;
    vector <int> v;
    int id(int x){return lower_bound(v.begin(),v.end(),x)-v.begin()+1;}
    void update(int l,int r,int &x,int y,int pos){
      t[++cnt]=t[y];t[cnt].sum++;x=cnt;
      if (l==r) return;
      int mid=(l+r)/2;
      if (pos<=mid) update(l,mid,t[x].l,t[y].l,pos);
      else update(mid+1,r,t[x].r,t[y].r,pos);
    }

    int query(int l,int r,int x,int y,int k){
      if (l==r) return l;
      int sum=t[t[y].l].sum-t[t[x].l].sum;
      int mid=(l+r)/2;
      if (k<=sum) return query(l,mid,t[x].l,t[y].l,k);
      else return query(mid+1,r,t[x].r,t[y].r,k-sum);
    }
    int main()
    {
        int i,x,y,z;
        scanf("%d %d",&n,&m);
        for (i=1;i<=n;i++) scanf("%d",&a[i]),v.push_back(a[i]);
        sort(v.begin(),v.end());v.erase(unique(v.begin(),v.end()),v.end());
        for (i=1;i<=n;i++) update(1,n,root[i],root[i-1],id(a[i]));
        while (m--){
          scanf("%d %d %d",&x,&y,&z);
          printf("%d\n",v[query(1,n,root[x-1],root[y],z)-1]);
        }
        return 0;
    }



方法二:划分树

首先排个序。框架还是线段树,但每个结点保证左子树的所有节点小于右子树的所有节点,并且每个节点内元素排列保持输入时的相对位置不变。以后询问区间(l,r),在线段树的每个节点算出(l,r)有多少落在了左子树,决定下一步向左还是向右,同时调整询问区间(l,r)以针对下一个线段树结点。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn=1e5+10;
int n,m;
int sorted[maxn],tree[30][maxn],toleft[30][maxn];
void build(int l,int r,int dep){
  int same,i,mid,lpos,rpos;
  if (l==r) return;
  mid=(l+r)/2;
  same=mid-l+1;   //计算=sorted[mid]的数个数,为了后续处理将左区间填满
  lpos=l;rpos=mid+1;
  for (i=l;i<=r;i++) if (tree[dep][i]<sorted[mid]) same--;
  for (i=l;i<=r;i++) {
    if (tree[dep][i]<sorted[mid]) tree[dep+1][lpos++]=tree[dep][i];
    else if (tree[dep][i]==sorted[mid] && same>0) {tree[dep+1][lpos++]=tree[dep][i];same--;}  //左区间有空余,需要sorted[mid]值填满
    else tree[dep+1][rpos++]=tree[dep][i];
    toleft[dep][i]=toleft[dep][l-1]+lpos-l;  
    //维护前缀和,以后在每一个二分区间即线段树结点时,
    //结点子区间(l,r)中划分至下一层左边的个数即为toleft[r]-toleft[l-1]
  }
  build(l,mid,dep+1);
  build(mid+1,r,dep+1);
}

int query(int L,int R,int l,int r,int dep,int k){   //(l,r)为在每一个[L,R]我们关心的数所在区间
  if (l==r) return tree[dep][l];
  int mid=(L+R)/2; 
  int cnt=toleft[dep][r]-toleft[dep][l-1];          //计算当前层(l,r)中划分至下一层左边的个数
  int newl,newr; 
  if (cnt>=k) {                                     //第k大数在左边
     newl=L+toleft[dep][l-1]-toleft[dep][L-1];newr=newl+cnt-1;  //维护新的(l,r),先确定l,再推r
     return query(L,mid,newl,newr,dep+1,k);
  }
  else {                                            //在右边
    newr=r+toleft[dep][R]-toleft[dep][r];           //维护新的(l,r),先确定r,将原先的r向右移动
                                                    //toleft[dep][R]-toleft[dep][r]个位置,再确定l
    newl=newr-(r-l-cnt);                            //现在区间缩小了cnt个
    return query(mid+1,R,newl,newr,dep+1,k-cnt);
  }
}
int main()
{
    int i,x,y,z;
    while (scanf("%d %d",&n,&m)!=-1){
        for (i=1;i<=n;i++) scanf("%d",&tree[0][i]),sorted[i]=tree[0][i];
        for (i=0;i<=22;i++) toleft[i][0]=0;
        sort(sorted+1,sorted+1+n);
        build(1,n,0);
        for (i=1;i<=m;i++){
            scanf("%d %d %d",&x,&y,&z);
            printf("%d\n",query(1,n,x,y,0,z));
        }
    }
    return 0;
}

Memory:14792KB   Time:1250MS,稍微快了点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值