【基础操作】整体二分概述

整体二分是一个常数小的离线做法。

这篇讲 $CDQ$ 的文章里提到了其一个分支——整体二分。

整体二分的适用性

有一些问题,在有多组操作(一开始赋初值也算操作)但只有一组询问的情况下(当然这组询问正常情况下就放在最后的,不然它后面的操作是摆着玩的),可以二分这个询问的答案。

二分的时间复杂度是 $O(log(n))$,验证一个答案是大了还是小了的时间复杂度是 $O(n)$(或者是这个级别的,差不多不到 $O(n^2)$ 就行),总时间复杂度是 $O(n imes log(n))$(或者是这个级别的)。

对于很多组询问的情况,设询问组数为 $Q$,如果对每组都二分,时间复杂度是 $O(Q imes n imes log(n))$ 级别的,对于祖传数据(全 $100000$)直接升天。

然后我们发现,有一些问题的修改对询问的影响非常有特性,支持高效维护和查询。这时候就可以把所有操作(包括修改和查询)放到一块二分。

总之,整体二分只能在满足以下条件的情况下使用:

  - 单组询问可以二分

  - 存在高效的数据结构维护修改对询问的影响(了解整体二分的应该知道,像区间修改这种的就不存在)

  - 题目可以离线做(废话)

  ……

 

主席树(模板)

询问静态区间第 $k$ 小。$n,Qle 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 imes 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
",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
",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
",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
",lsh[ans[i]]);
70     return 0;
71 }
View Code

Dynamic Rankings(zoj2112)

询问动态区间第 $k$ 小。

题解

我们发现,既然可以把所有操作都混到一块,为什么不能在询问中间插入一些修改操作呢?

修改操作可以看成减一个数,再加一个数。因此把每个修改操作拆成减去原来的数、加上新的数即可。

注意,对于减去的数,要按它的绝对值进行整体二分的递归判断。因为之前肯定加过这个数,之前那个数对 $mid$ 有影响(给线段树对应数位加 $1$ 什么的),这个数就对 $mid$ 有影响(比如给线段树对应数位 $-1$,去掉之前那个数的影响),也就是说这个数实际上就是消去之前加上的那个数的影响,之前那个数在哪,这个数就得在哪。(可以用反证法证明这个性质,想想如果这对加减操作不在同一个操作区间,减法后面的操作会发生什么)

后记

其实大部分整体二分的题目都完全可以用纯数据结构(比如主席树)代替,写整体二分只是为了减小常数。

了解一下,会写就好了。

原文地址:https://www.cnblogs.com/scx2015noip-as-php/p/whole_dichotomy.html