例题地址 K-th Number(POJ 2104)
参考挑战程序设计一书中的内容。
平方分割的方法,将所要查询的区间分割成接近
Nmax−−−−√
N
m
a
x
长度的区间,对于每一个分割的区间进行排序。当要查询一个新的区间时,利用二分的思想,二分答案,首先先处理区间的两端,将两端的部分一一访问寻找比当前搜索值
x
x
小的值的个数,然后访问桶中比小的值,由于桶中的值都是排好序的,所以我们可以利用
upperbound
u
p
p
e
r
b
o
u
n
d
函数在
O(number(bucket)⋅log(bucket.length))
O
(
n
u
m
b
e
r
(
b
u
c
k
e
t
)
·
l
o
g
(
b
u
c
k
e
t
.
l
e
n
g
t
h
)
)
的复杂度中找到比当前询问的值小的值的个数。通过check当前区间比x小的值的个数和查询的数量来判断下一个收缩区间。寻找区间K大值的复杂度为
O(n⋅logn+m⋅n−−√⋅log1.5n)
O
(
n
·
log
n
+
m
·
n
·
log
1.5
n
)
#include <vector>
#include <algorithm>
#include <cstdio>
#define PB(x) push_back(x)
#define all(x) (x).begin(),(x).end()
using namespace std;
const int MAX_N=100000+100;
const int MAX_M=5000+100;
const int B = 1000;
int n,m;
int A[MAX_N];//原数组
int I[MAX_M],J[MAX_M],K[MAX_M];//储存询问的左右区间和K的值
int nums[MAX_N];//原数组排序好的数组
vector<int>bucket[MAX_N/B];//桶
void Solve()
{
for(int i=0;i<n;i++){
bucket[i/B].PB(A[i]);//将元素推入桶中
nums[i]=A[i];
}
sort(nums,nums+n);
for(int i=0;i<n/B;i++) sort(bucket[i].begin(),bucket[i].end());
//对桶中数据进行排序
for(int i=0;i<m;i++){
int l=I[i]-1,r=J[i],k=K[i];
//由于数组是0开始的,所以要对访问的区间进行预处理
int lb=-1,ub=n-1;
while(ub-lb>1){//二分
int md=(ub+lb)/2;
int x=nums[md];
int tl=l,tr=r,c=0;
while(tl<tr&&tl%B) if(A[tl++]<=x)c++;//处理左端
while(tl<tr&&tr%B) if(A[--tr]<=x)c++;//处理右端
while(tl<tr){//处理中间部分桶中的信息
int b=tl/B;
c+=lower_bound(all(bucket[b]),x)-bucket[b].begin();//通过upperbound函数寻找比当前值小的值的个数
tl+=B;//访问下一个桶
}
if(c>=k) ub=md;
else lb=md;
}
printf("%d\n",nums[ub]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++) scanf("%d",&A[i]);
for(int i=0;i<m;i++) scanf("%d%d%d",&I[i],&J[i],&K[i]);
Solve();
return 0;
}
划分树写法复杂度 O(n⋅logn+m⋅log3n) O ( n · log n + m · log 3 n ) 速度略高于分块。划分树的思想类似于分桶法,类似于线段树的二分思想。对于每一个点所维护的信息是一段长度的数组,对于每个询问,可以在 log3n l o g 3 n 的复杂度中寻找到答案,通过划分树的做法可以将大区间的询问时间缩短,即可以通过区间合并的方法,一次访问多个桶中的情况。其中有两个新的函数 resize() r e s i z e ( ) 和 merge() m e r g e ( ) ,前者可以通过重新规划vector的空间减少申请空间的次数,后者可以通过合并两个vector然后将所有的元素进行排序,然后达到将多个桶的内容”绑”在一起,方便之后询问操作。
#include <vector>
#include <algorithm>
#include <cstdio>
#define PB(x) push_back(x)
#define all(x) (x).begin(),(x).end()
using namespace std;
const int MAX_N=100000+100;
const int MAX_M=5000+100;
const int ST_SIZE=(1<<18)-1;
int N,M;
int A[MAX_N];
int I[MAX_M],J[MAX_M],K[MAX_M];
int nums[MAX_N];
vector<int>dat[ST_SIZE];
void init(int k,int l,int r){
if(r-l==1){
dat[k].PB(A[l]);
}
else {
int lch = k*2+1,rch=k*2+2;
init(lch,l,(l+r)/2);
init(rch,(l+r)/2,r);
dat[k].resize(r-l);//对于每一个父节点申请一个固定长度的区间长度用来储存两个子区间的合并元素
merge(all(dat[lch]),all(dat[rch]),dat[k].begin());//合并两个子区间(合并以后的区间时排好序的)
}
}
int query(int i,int j,int x,int k,int l,int r){
if(j<=l||r<=i) return 0;
else if(i<=l&&r<=j){
return upper_bound(all(dat[k]),x)-dat[k].begin();//返回每个桶中比当前值小的值的个数
}else {
int lc=query(i,j,x,k*2+1,l,(l+r)/2);
int rc=query(i,j,x,k*2+2,(l+r)/2,r);
return rc+lc;
}
}
void Solve(){
for(int i=0;i<N;i++) nums[i]=A[i];
sort(nums,nums+N);
init(0,0,N);
for(int i=0;i<M;i++){
int l=I[i]-1,r=J[i],k=K[i];
int lb=-1,ub=N-1;
while(ub-lb>1){
int md=(ub+lb)/2;
int c=query(l,r,nums[md],0,0,N);
if(c>=k)ub=md;
else lb=md;
}
printf("%d\n",nums[ub]);
}
}
int main()
{
scanf("%d%d",&N,&M);
for(int i=0;i<N;i++) scanf("%d",&A[i]);
for(int i=0;i<M;i++) scanf("%d%d%d",&I[i],&J[i],&K[i]);
Solve();
return 0;
}