整体二分是一个常数小的离线做法。
这篇讲 $CDQ$ 的文章里提到了其一个分支——整体二分。
整体二分的适用性
有一些问题,在有多组操作(一开始赋初值也算操作)但只有一组询问的情况下(当然这组询问正常情况下就放在最后的,不然它后面的操作是摆着玩的),可以二分这个询问的答案。
二分的时间复杂度是 $O(log(n))$,验证一个答案是大了还是小了的时间复杂度是 $O(n)$(或者是这个级别的,差不多不到 $O(n^2)$ 就行),总时间复杂度是 $O(n\times log(n))$(或者是这个级别的)。
对于很多组询问的情况,设询问组数为 $Q$,如果对每组都二分,时间复杂度是 $O(Q\times n\times log(n))$ 级别的,对于祖传数据(全 $100000$)直接升天。
然后我们发现,有一些问题的修改对询问的影响非常有特性,支持高效维护和查询。这时候就可以把所有操作(包括修改和查询)放到一块二分。
总之,整体二分只能在满足以下条件的情况下使用:
- 单组询问可以二分
- 存在高效的数据结构维护修改对询问的影响(了解整体二分的应该知道,像区间修改这种的就不存在)
- 题目可以离线做(废话)
……
主席树(模板)
询问静态区间第 $k$ 小。$n,Q\le 200000$。
题解
其实也是个整体二分模板题。
如果只有一组询问,我们可以二分答案,判断的话就扫一遍询问区间,看有几个数比这个答案小,就可以确定这个答案是区间第几小了。
对于多组询问的话,我们可以把所有操作扔到一起二分。
具体是什么意思呢?
举个例子,给出一个数 $x$,在二分答案 $mid$ 时,它只会对所有大于 $mid$ 的答案造成影响。也就是说它跟询问一样,可以被二分。
下面说说具体操作:
我们按输入的顺序,依次模拟当前操作区间的操作。(在模拟当前区间的所有操作之前,把之前二分时记录的信息都清空,即假装之前啥操作也没做过)
如果这个操作是赋值操作(在这题就是一开始给数组赋值),
如果赋的数小于等于 $mid$,设这个数为 $x$,把它在树状数组/线段树的第 $x$ 位 $+1$,并把这个操作扔到左递归区间(即 $[l,mid]$ 答案区间);
如果赋的数大于 $mid$,直接把这个操作扔到右递归区间(即 $(mid,r]$ 区间)。
如果这个操作是询问操作,
先用树状数组/线段树对其询问区间求和(这就是之前为什么说在问题比较简单时 才适合整体二分,你得确保能再用个数据结构维护),也就是查其询问区间当前有多少个小于等于 $mid$ 的数,这里设有 $x$ 个。
如果 $x$ 小于该查询的 $k$(即这个查询问的是区间第 $k$ 小),就把该询问的 $k$ 减去 $x$(左递归区间有足够的数小于 $mid$,这个询问的答案显然在右递归区间,于是减去左区间的数的影响),并把这个操作扔到右递归区间;
如果 $x$ 大于等于该查询的 $k$,直接把这个操作扔到左递归区间。
如果二分答案的区间 $[l,r]$ 递归到了 $l=r$,就把扔到这个递归区间的所有询问操作的答案都赋为 $l$ 或 $r$,回溯。
$Q1:$ 为什么对于赋值操作,赋的数小于等于 $mid$ 时,要把操作扔到左递归区间?
$A1:$ 因为我们在询问操作中,把要递归到右子区间的询问 减掉了小于等于 $mid$ 的数对询问的影响,也就是说这个赋值操作以后不用管递归到右子区间的询问操作了,于是扔到左子区间即可。
$Q2:$ 那赋的数大于 $mid$ 时,要把操作扔到右递归区间?
$A2:$ 因为这个数不会对询问答案在左子区间的询问造成影响,它不会参与这些询问的排名。
其它证明都比较显然了吧(其实是我没时间写)
对于这题,如果不离散化的话,只能用权值线段树;如果离散化的话,树状数组和权值线段树就都可以用了。
注意一个事情,一开始就要把所有操作的数改为离散化后的数,不要在整体二分里再对每个数套 $map$ 查询,因为整体二分的时间复杂度顶多做到 $O(n\times log^2(n))$,再套个 $map$ 的 $log$ 可能会爆炸。
1 #include<bits/stdc++.h> 2 #define rep(i,x,y) for(int i=x;i<=y;++i) 3 #define dwn(i,x,y) for(int i=x;i>=y;--i) 4 #define ll long long 5 #define N 400001 6 using namespace std; 7 inline int read(){ 8 int x=0; bool f=1; char c=getchar(); 9 for(;!isdigit(c);c=getchar()) if(c=='-') f=0; 10 for(; isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^'0'); 11 if(f) return x; 12 return 0-x; 13 } 14 int n,m,a[N],b[N],ans[N],cnt; 15 map<int,int>mp; int lsh[N]; 16 struct query{int x,l,r,k;}q[N],L[N],R[N]; 17 namespace Fwk{ 18 int fwk[N]; 19 inline int lowbit(int x){return x&(-x);} 20 inline void add(int x,int v){ 21 for(;x<=n;x+=lowbit(x)) fwk[x]+=v; 22 } 23 inline int query(int x){ 24 int ret=0; 25 for(;x>0;x-=lowbit(x)) ret+=fwk[x]; 26 return ret; 27 } 28 }; 29 void solve(int ql,int qr,int l,int r){ 30 //printf("solve:%d %d %d %d\n",ql,qr,l,r); 31 if(l==r){ 32 rep(i,ql,qr) if(q[i].x) ans[q[i].x]=l; 33 return; 34 } 35 int mid=(l+r)>>1,cntL=0,cntR=0,tmp; 36 rep(i,ql,qr){ 37 //cout<<"faq:"<<Fwk::query(2)<<' '<<Fwk::query(0)<<endl; 38 if(!q[i].x){ 39 if(q[i].k<=mid) L[++cntL]=q[i], Fwk::add(q[i].l,1); 40 else R[++cntR]=q[i]; 41 //printf("1:%d %d %d\n",q[i].l,q[i].k,mid); 42 } 43 else{ 44 tmp=Fwk::query(q[i].r)-Fwk::query(q[i].l-1); 45 if(tmp<q[i].k) q[i].k-=tmp, R[++cntR]=q[i]; 46 else L[++cntL]=q[i]; 47 //printf("2:%d %d %d %d %d\n",q[i].x,q[i].l,q[i].r,q[i].k,tmp); 48 } 49 } 50 rep(i,1,cntL){ 51 q[ql-1+i]=L[i]; 52 if(!L[i].x) Fwk::add(L[i].l,-1); 53 } 54 rep(i,1,cntR) q[ql-1+cntL+i]=R[i]; 55 solve(ql,ql-1+cntL,l,mid), solve(ql-1+cntL+1,qr,mid+1,r); 56 } 57 int main(){ 58 //freopen("luogu3834.in","r",stdin); 59 //freopen("luogu3834.out","w",stdout); 60 n=read(),m=read(); 61 rep(i,1,n) b[i]=read(), q[i]=(query){0,i,i,b[i]}; 62 sort(b+1,b+n+1); 63 mp[b[1]]=++cnt, lsh[cnt]=b[1]; 64 rep(i,2,n) if(b[i]!=b[i-1]) mp[b[i]]=++cnt, lsh[cnt]=b[i]; 65 int l,r,k; 66 rep(i,1,m) l=read(),r=read(),k=read(), q[n+i]=(query){i,l,r,k}; 67 rep(i,1,n) q[i].k=mp[q[i].k]; 68 solve(1,n+m,1,cnt); 69 rep(i,1,m) printf("%d\n",lsh[ans[i]]); 70 return 0; 71 }
Dynamic Rankings(zoj2112)
询问动态区间第 $k$ 小。
题解
我们发现,既然可以把所有操作都混到一块,为什么不能在询问中间插入一些修改操作呢?
修改操作可以看成减一个数,再加一个数。因此把每个修改操作拆成减去原来的数、加上新的数即可。
注意,对于减去的数,要按它的绝对值进行整体二分的递归判断。因为之前肯定加过这个数,之前那个数对 $mid$ 有影响(给线段树对应数位加 $1$ 什么的),这个数就对 $mid$ 有影响(比如给线段树对应数位 $-1$,去掉之前那个数的影响),也就是说这个数实际上就是消去之前加上的那个数的影响,之前那个数在哪,这个数就得在哪。(可以用反证法证明这个性质,想想如果这对加减操作不在同一个操作区间,减法后面的操作会发生什么)
后记
其实大部分整体二分的题目都完全可以用纯数据结构(比如主席树)代替,写整体二分只是为了减小常数。
了解一下,会写就好了。