题目大意:
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,稍微快了点