区间第K值带修改 分块

来源:互联网 发布:java读取公钥pem文件 编辑:程序博客网 时间:2024/05/16 14:16

区间第K值带修改

Q:给定一个序列 1、查询一个L到R的区间内的第K大(小)值。2、修改一个值。

这道神题困扰了我很久,直到学了各种各样的算法才会这道题。

先比较一下流行的算法的效率吧

对于一个长度为N的序列

---------------------------------------------------

暴力:对于一段查询区间每次进行快排。

时间复杂度:

单次查询:O(NlogN)

修改:O(1)

空间复杂度:O(N)

----------------------------------------------------

线段树套平衡树:

单次查询:O(log^3 N)

修改:O(log^2 N)

空间复杂度:O(N^2logn)

-----------------------------------------------------

主席树(函数式线段树):

单次询问:O(log^2 N)

修改:O(log^2 N)

空间复杂度:???

-----------------------------------------------------

上面这几种算法

暴力最好写可惜太慢了拿不了分(你这不是说废话吗发火)

树套树.......编程复杂度过高、难于调试.......不想写

函数式线段树???没写过,不知道怎么样看起来最Work,可惜我不会写(听了冬令营CLJ大神的Functional LCT后就对函数式编程留下了阴影大笑

那怎么办???有没有我能懂又好写又能骗到较高分数的算法呢???

先考虑离线,这题能不能离线呢,没有修改的话用莫队算法+平衡树(时间复杂度O(Nsqrt(N)log N))可以(没修改有更高效的在线算法O(MlogN),划分树),但是有修改可就麻烦了,我们离线做的时候不知道查询的数在第几个修改之后,假若用可持久化数组来维护时间戳也不知道当前区间内的数有没有被修改过......我苦思冥想不得解决办法,看来不能离线做。

某天又逛了逛神犇的blog突然想到可以分块解决。

-----------------------------------------------------以下是正文--------------------------------------------------------------------------------------

分块做法:我们把序列分成Sqrt(N)个块,每个块内进行排序。

对于修改,在原序列中修改,在块内二分查找原来的数再进行修改,并把修改后的数在块内移动到对应位置。

在线回答每个询问,如果询问的区间在同一个块内,便直接在原序列上二分答案,暴力统计区间内有多少个数大于(小于)二分出的答案

如果询问的区间不在同一个块内,对于两边的做法和上面相同,对于所有被区间覆盖的块在块内进行二分查找。

这样我们就完成了整个操作。

例子:

序列        8 5 7 8 3 6 7 5 1 2

分块排序       【5 7 8】【3 6 8】【1 5 7】【2】

查询[1,3]内第二小的数

[1-3]在同一个块内,直接在原序列中暴力统计,二分答案下界取0,上界取8(上下界选取看题)

第一次二分出答案4,比4小的有0个

第二次二分6,比6小的有1个

第三次7,比7小的有1个

第四次,R=L=8,答案=8-1=7;


修改第5个数为1 

在【3 6 8】块内二分查找6,把6改成1【3 1 8】向左移动得到【1 3 8】

修改原序列8 5 7 8 3 1 8 1 5 7 2


查询[2,8]内第3小的数

对于查询左边的[2,3]和右边的[7,8]向上面一样暴力统计,中间的一个块[4,6]二分查找,就可以得到答案

-----------------------------------------时间复杂度分析---------------------------------------------------------------------------

预处理:O(N/2 Log(N) )(对Sqrt(N)个块内的Sqrt(N)个元素进行排序)

修改:O(1/2Log(N)+Sqrt(N)) (在块内二分查找,并移动)

查询:O(Log(U)Sqrt(N)+1/2Log(U)Sqrt(N)Log(N))(U为上界-下界,两边暴力统计,中间二分查找)


算法优点:

1、编程复杂度小,考场上时间就是生命

2、空间复杂度 O(N),比树套树的啥的好多了

3、分块思想广泛应用于解题中!!!

4、常数小

------------------------------------------------------------下面是代码----------------------------------------------------------------------

BZOJ 1901

Accepted1600 kb1092 ms
#include <cstdio>#include <cstdlib>#include <cmath>#include <ctime>#include <cstring>#include <algorithm>#define MAX 100002using namespace std;int n,ub=0,lb=0x7fffffff,kuai;//ub上界,lb下界,kuai块的大小 int num[MAX],divd[MAX];//num原序列,divd分块数组 int b_s(int l,int r,int a){//二分查找 while(l!=r){int m=(l+r)>>1;if(divd[m]>=a)r=m;else l=m+1;}if(divd[l]>=a)--l;return l;}void change(int pos,int a){int l=((pos-1)/kuai)*kuai+1;int r=min(n,l+kuai-1);int ks=b_s(l,r,num[pos])+1;//二分查找位置 divd[ks]=a;while(divd[ks]<divd[ks-1]&&ks-1>=l){swap(divd[ks],divd[ks-1]);--ks;}while(divd[ks]>divd[ks+1]&&ks+1<=r){swap(divd[ks],divd[ks+1]);++ks;}//移动到拍好序的位置 num[pos]=a;}int query(int l,int r,int k){int lk=((l-1)/kuai)+1,rk=((r-1)/kuai)-1;//确定查询的块 int d=lb,u=ub;while(d!=u){//二分答案 int m=(d+u)>>1;int kth=0;if(lk<=rk){ //在两边直接查找,块内二分查找 for(int i=l;i<=lk*kuai;i++)if(num[i]<m)++kth;for(int i=lk;i<=rk;i++)kth+=b_s(i*kuai+1,i*kuai+kuai,m)-i*kuai;for(int i=rk*kuai+kuai+1;i<=r;i++)if(num[i]<m)++kth;}else for(int i=l;i<=r;i++)if(num[i]<m)++kth;//在同一块内 if(kth>=k)u=m;else d=m+1;}return d-1;}int main(){//freopen("kth.in","r",stdin);int m;scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){scanf("%d",&num[i]);ub=max(ub,num[i]+1);lb=min(lb,num[i]-1);}//输入,确定上下界 memcpy(divd,num,sizeof num);kuai=sqrt(n+0.5);for(int i=0;i<=kuai;i++)sort(divd+i*kuai+1,divd+min(n,(i+1)*kuai)+1);//分块并排序 while(m--){int r,l,k;char q;do{scanf("%c",&q);}while(q!='C'&&q!='Q');if(q=='C'){//修改 scanf("%d%d",&l,&k);change(l,k);if(k+1>ub)ub=k+1;if(k-1<lb)lb=k-1;//更新上下界 }else{//查询 scanf("%d%d%d",&l,&r,&k);printf("%d\n",query(l,r,k));}}//printf("\nRuntime: %d ms",clock());return 0;}


0 0
原创粉丝点击