数据结构专题
做法有多种:
1、二分+分块
2、二分+归并树
3、划分树
4、主席树
其中第3种我没有写。
这四种方法各自的时间空间复杂度都不一样,推广性也不一样。
时间:
二分+分块:O(nlog√(nlogn)+m√nlog1.5n)
二分+归并树:O(nlogn+mlog3n)
主席树:O( (n+m)logn )
推广性:
二分+分块:推广性强,效率不高
二分+归并树:推广性弱,效率较高
主席树:推广性极强,效率极高
现在分别讲解这三种方法
一、二分+分块:
分两块:二分和分块(说和没说一样)
我们发现一个对于一个数x,x越大,那么他的排名就越靠前,那么就是线性的,可以用二分。
二分后就要判断其是第几大数。如果一个一个判断过来,不如直接写。可以用分块。把整个分成几块后,将每一块里的数都进行排序。排序有什么好处?可以用二分查找(lower_ bound或upper_ bound,具体看题目)。这样就很高效了。别忘了一些不在完整块里的残留要循环过去。
还有一点是要离散,数据很大,不离散内存会炸。
Code:
#include<bits/stdc++.h>
using namespace std;
int A[30005],B[30005],n,cas,N,C[30005];
bool check(int l,int r,int x,int P){//分块求值
int L=l/N,R=r/N,res=0;
for(int i=L+1;i<R;i++){//完整块内
int sum=lower_bound(A+i*N+1,A+(i+1)*N+1,x)-A-1-i*N;
res+=sum;
}
if(L!=R){//块外
for(int i=l;i<=(L+1)*N;i++)if(C[i]<x)res++;
for(int i=R*N+1;i<=r;i++)if(C[i]<x)res++;
}else{
for(int i=l;i<=r;i++)if(C[i]<x)res++;
}
res=r-l+1-res;
if(res>=P)return 1;
return 0;
}
int main(){
scanf("%d%d",&n,&cas);
for(int i=1;i<=n;i++)scanf("%d",&A[i]),B[i]=A[i],C[i]=A[i];
sort(B+1,B+1+n);
N=sqrt(n);//这样的N其实不是最优的,最优的是N=sqrt(n*log2(n)),可以手推一下在这道题里这样最快
for(int i=0;i<N;i++)sort(A+N*i+1,A+N*(i+1)+1);//每一块排序
sort(A+N*N+1,A+1+n);//最后一块不完整,单独排序
while(cas--){
int l,r,k;
scanf("%d%d%d",&l,&r,&k);
int L=1,R=n,ans;
while(L<=R){//二分枚举一个x
int mid=(L+R)/2;
if(check(l,r,B[mid],k))L=mid+1,ans=mid;
else R=mid-1;
}
printf("%d\n",B[ans]);//找到的只是离散后的下标
}
return 0;
}
二、二分+归并树
二分和上面的是一样的。
归并树是新学的。个人的理解是把归并排序时这一层的状态全部记录下来,而不是全部排完。这样就可以按照线段树的查询方式往下递归,然后再用二分查找求答案。
Code:
#include<bits/stdc++.h>
#define M 30005
using namespace std;
int A[M],S[20][M],B[M];
struct node{int L,R;}tree[M*4];
void Build(int L,int R,int p,int dep){//建树
tree[p].L=L;tree[p].R=R;
if(L==R){
S[dep][L]=A[L];
return;
}
int mid=(L+R)/2;
Build(L,mid,p*2,dep+1);Build(mid+1,R,p*2+1,dep+1);//先把后面的算出来,和归并排序时一样的
int i=L,j=mid+1,k=L;
while(i<=mid&&j<=R)
if(S[dep+1][i]<S[dep+1][j])S[dep][k++]=S[dep+1][i++];
else S[dep][k++]=S[dep+1][j++];
while(i<=mid)S[dep][k++]=S[dep+1][i++];
while(j<=R)S[dep][k++]=S[dep+1][j++];
}
int Query(int L,int R,int p,int dep,int x){//和线段树查询基本一样
if(tree[p].L==L&&tree[p].R==R){
return lower_bound(S[dep]+L,S[dep]+R+1,x)-S[dep]-L;
}
int mid=(tree[p].L+tree[p].R)/2;
if(mid>=R)return Query(L,R,p*2,dep+1,x);
else if(mid<L)return Query(L,R,p*2+1,dep+1,x);
else return Query(L,mid,p*2,dep+1,x)+Query(mid+1,R,p*2+1,dep+1,x);
}
int main(){
int n,cas;
scanf("%d%d",&n,&cas);
for(int i=1;i<=n;i++)scanf("%d",&A[i]),B[i]=A[i];
sort(B+1,B+1+n);
Build(1,n,1,1);
while(cas--){//此段与上一个同类
int l,r,x;
scanf("%d%d%d",&l,&r,&x);
int L=1,R=n,ans=0;
while(L<=R){
int mid=(L+R)/2;
int res=r-l+1-Query(l,r,1,1,B[mid]);
if(res>=x)L=mid+1,ans=mid;
else R=mid-1;
}
printf("%d\n",B[ans]);
}
return 0;
}
三、主席树
这个才是本篇的重点。虽然我依旧不是很会主席树,但是手推的话大致也是懂得了那么一点点。主席树就是一种权值前缀和,但是如果把每个前缀和造成一棵树,那么内存就太耗了。如果把数全部画出来后,就可以发现其实有很多都是重复的。那么如何利用这些重复的点?很容易想到的就是共用这些点。那么如何实现?其实我也不是很会……(读者还是上网自己查吧,等以后我再补)
这道题的主席树是静态的,即不能更新(插入不算更新),反正这道题只有查询。
Code:
#include<bits/stdc++.h>
#define M 30005
using namespace std;
int A[M],B[M],Lt[M*20],Rt[M*20],Gt[M*20],Sum[M*20],tot;//tot是节点的编号
void Build(int L,int R,int &tid){//造树
tid=++tot;Sum[tid]=0;
if(L==R)return;
int mid=(L+R)/2;
Build(L,mid,Lt[tid]);Build(mid+1,R,Rt[tid]);
}
void Insert(int lat,int L,int R,int x,int &tid){//插入一个新数
tid=++tot;
Lt[tid]=Lt[lat];Rt[tid]=Rt[lat];//这些大概是就是把不共用的造出来,公用的不变
Sum[tid]=Sum[lat]+1;
if(L==R)return;
int mid=(L+R)/2;
if(mid>=x)Insert(Lt[lat],L,mid,x,Lt[tid]);
else Insert(Rt[lat],mid+1,R,x,Rt[tid]);
}
int Query(int lt,int rt,int L,int R,int x){
if(L==R)return L;
int cnt=Sum[Lt[rt]]-Sum[Lt[lt]];
int mid=(L+R)/2;
if(x<=cnt)return Query(Lt[lt],Lt[rt],L,mid,x);
else return Query(Rt[lt],Rt[rt],mid+1,R,x-cnt);
}
int main(){
int n,cas;
scanf("%d%d",&n,&cas);
for(int i=1;i<=n;i++)scanf("%d",&A[i]),B[i]=A[i];
sort(B+1,B+1+n);int m=unique(B+1,B+1+n)-B-1;//离线去重都是必须的
for(int i=1;i<=n;i++)A[i]=lower_bound(B+1,B+1+m,A[i])-B;
Build(1,m,Gt[0]);//现造一棵空树
for(int i=1;i<=n;i++)Insert(Gt[i-1],1,m,A[i],Gt[i]);//每次插入一个点
while(cas--){
int l,r,x;
scanf("%d%d%d",&l,&r,&x);
printf("%d\n",B[Query(Gt[l-1],Gt[r],1,m,r-l+2-x)]);
}
return 0;
}
这是数据结构专题的第一篇,之后还有其他的,都是一些数据结构来优化程序。