二分答案 / 整体二分学习笔记
本讲内容分为两个部分;
第1部分:二分答案介绍(淦,不是讲PJ算法)
第2部分:整体二分介绍(我们是讲整体二分)
Part 1 二分答案介绍
相信大家都对二分答案比较熟悉。
至少...需要会二分查找...
我们考虑对于决策单调的问题:
给出一个长度为n的递增数列,现在有m个询问,每个询问有一个参数$k_i$
对于每一个询问输出这个递增序列中的大于等于$k_i$的位置最靠前的项的位置。
如果没有输出$-1$
对于100%的数据$ n,m \leq 10^6$
这个就大概是二分查找的模板题了,我们当考虑的是当前搜区间是$[L,R]$
其中间项$M=\left \lfloor \frac{L+R}{2} \right \rfloor$大于还是小于$k_i$
如果严格$a_M \geq k_i$,那么搜$[L,M]$否则搜$[M+1,R]$
显然这段代码就可以干这件事情
# include <bits/stdc++.h> using namespace std; const int N=1e6+10; int a[N],n,m; int work(int k) { int l=1,r=n,ans=-1; while (l<=r) { int m=(l+r)/2; if (a[m]>=k) r=m-1,ans=m; else l=m+1; } return ans; } int main() { scanf("%d%d",&n,&m); for (int i=1;i<=n;i++) scanf("%d",&a[i]); while (m--) { int k; scanf("%d",&k); printf("%d\n",work(k)); } return 0; }
不作赘述。
满足决策单调性的事情可以用二分答案转化为判定类问题。
给出二分答案模板:
int l=-inf,r=inf,ans; while (l<=r) { int mid=(l+r)>>1; if (check(mid)) ans=mid,l=mid+1; //向更优解尝试 else r=mid-1; //向更劣解尝试 } return ans;
然而今天的重点不是这个,这个有点low
Part 2 整体二分介绍
看完上面二分答案是不是感觉这篇blog在水?
其实没有,整体二分是二分答案的加强版更加优美、便捷、不易打挂。
其实本质就是对于操作区间$[ql,qr]$,保证其答案在$[L,R]$中,然后通过$[L,R]$的二分然后逐步缩小,直到L=R,求出$[ql,qr]$的答案是L
上面讨论了一个最显然的问题,考虑如果在上述定义下$[ql,qr]$,保证其答案在$[L,R]$中,当$L=R$时有,$[ql,qr]$答案都为L
对于每一个属于[ql,qr]的询问,考虑当前答案$M=\left \lfloor \frac{L+R}{2} \right \rfloor$是否对其有贡献,如果有累加贡献。
然后对于每一个属于[ql,qr]的询问,考虑之前累加贡献+新增贡献和期望贡献值比较,
如果期望贡献值 >= 累加贡献+新增贡献 那么答案就需要比M要大or等(这里按照正相关)
如果期望贡献值 <= 累加贡献+新增贡献 那么答案就需要比M要小or等(这里按照正相关)
然后回退此次操作的更新数据结构的值,保证复杂度只和qr-ql+1有关
最后期望答案小于等于M的和期望答案大于M的分别分治solve即可。
还记得上次[可持久化数据结构学习笔记]中学习的主席树吗?
题目1 : 给出模板题目: P3834 【模板】可持久化线段树 1(主席树)
我们使用树状数组维护当前的贡献[本质是维护位置,值才是贡献],这里的累加贡献和新增贡献就是[x,y]树状数组维护的区间和
这里的期望贡献值就是K。
主要是循环里面讨论的问题:
1.是更改操作[pos,x]
如果 更改位置上的值x > M 那么对贡献无影响,放到q2里面 , 更大的才会用到他
如果 更改位置上的值x <=M 那么对答案有影响,放到q1里面,已经加入更大的已经加过了[表现为减去...],在树状数组维护的相应位置上pos + 1
2.是查询操作[l,r,k]
设w为 查询 [l,r]树状数组维护有w个答案在此下标范围内。
如果 w <= k 那么答案应该比这个M更小或等 , 放到q1里面
如果 w>k 那么答案应该比M更大 , 减去当前查询已知有这么多[更大加过的表现],放到q2里面,查询更大答案。
代码Code:请结合上述注释食用
# include <bits/stdc++.h> # define inf (0x7f7f7f7f) using namespace std; const int N=2e5+10; struct rec{int l,r,k,id,type,val;}a[N<<1],q1[N<<1],q2[N<<1]; int ans[N<<1],n,m,tot; # define lowbit(x) (x&(-x)) int c[N]; inline int read() { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); return w?-X:X; } inline void write(int x) { if (x<0) { x=-x; putchar('-');} if (x>9) write(x/10); putchar('0'+x%10); } void update(int x,int y){for (;x<=n;x+=lowbit(x)) c[x]+=y;} int query(int x){int ret=0;for (;x;x-=lowbit(x)) ret+=c[x];return ret;} # undef lowbit void solve(int ql,int qr,int L,int R) { if (ql>qr) return; if (L==R) { for (int i=ql;i<=qr;i++) if (a[i].type==2) ans[a[i].id]=L; return; } int M=(L+R)>>1,t1=0,t2=0; for (int i=ql;i<=qr;i++) if (a[i].type==1) { if (a[i].val<=M) update(a[i].id,1),q1[++t1]=a[i]; else q2[++t2]=a[i]; } else if (a[i].type==2) { int num=query(a[i].r)-query(a[i].l-1); if (num>=a[i].k) q1[++t1]=a[i]; else a[i].k-=num,q2[++t2]=a[i]; } for (int i=1;i<=t1;i++) if (q1[i].type==1) update(q1[i].id,-1); for (int i=1;i<=t1;i++) a[ql+i-1]=q1[i]; for (int i=1;i<=t2;i++) a[ql+t1+i-1]=q2[i]; solve(ql,ql+t1-1,L,M); solve(ql+t1,qr,M+1,R); } int main() { n=read();m=read(); for (int i=1;i<=n;i++) { int t=read(); a[++tot].id=tot; a[tot].type=1; a[tot].val=t; } for (int i=1;i<=m;i++) { a[++tot].type=2;a[tot].l=read(); a[tot].r=read();a[tot].k=read();a[tot].id=tot; } solve(1,tot,-inf,inf); for (int i=n+1;i<=tot;i++) write(ans[i]),putchar('\n'); return 0; }
这个树状数组套可持久化线段树还记不记得,其实用整体二分是很简单实现的。
就是把删除操作拆分成2个操作,删(减去原来的)和加(加上现在的)
我们把一定部分的代码做出了修改,主要是在树状数组加入(+y)和删除(-y)上
对于每一个2操作多了一个参数y表示是删除(-1)还是加上(1)
代码Code:请结合上述题目1和上述注释食用:
# include <bits/stdc++.h> # define fp(i,s,t) for(int i=s;i<=t;i++) using namespace std; const int N=(1e5+5)*3; const int inf=1e9; struct rec{ int x,y,k,op,id; //如果是1操作 值,加/减,无,操作编号,答案编号 //如果是2操作 左边界,右边界,k小值,操作编号,数组中位置编号 }a[N],q1[N],q2[N]; int tmp[N],ans[N],n,m,tot; # define lowbit(x) (x&(-x)) int c[N]; void update(int x,int y) { for (;x<=n;x+=lowbit(x)) c[x]+=y;} int query(int x) {int ret=0;for (;x;x-=lowbit(x)) ret+=c[x];return ret;} # undef lowbit void solve(int ql,int qr,int L,int R) { if (ql>qr) return; if (L==R) { fp(i,ql,qr) if (a[i].op==2) ans[a[i].id]=L; return; } int M=(L+R)>>1,t1=0,t2=0; fp(i,ql,qr) if (a[i].op==1) { if (a[i].x<=M) { update(a[i].id,a[i].y); //*加上a[i].y q1[++t1]=a[i]; } else q2[++t2]=a[i]; } else { int num=query(a[i].y)-query(a[i].x-1); if (num>=a[i].k) q1[++t1]=a[i]; else a[i].k-=num,q2[++t2]=a[i]; } for (int i=1;i<=t1;i++) if (q1[i].op==1) update(q1[i].id,-q1[i].y);//*加上-a[i].y还原 fp(i,1,t1) a[ql+i-1]=q1[i]; fp(i,1,t2) a[ql+t1+i-1]=q2[i]; solve(ql,ql+t1-1,L,M); solve(ql+t1,qr,M+1,R); } int main() { scanf("%d%d",&n,&m); fp(i,1,n) { scanf("%d",&tmp[i]); a[++tot]=(rec) {tmp[i],1,0,1,i}; } int qes=0; fp(i,1,m) { char c=0; while (c!='Q'&&c!='C') c=getchar(); if (c=='Q') { int l,r,k; scanf("%d%d%d",&l,&r,&k); a[++tot]=(rec) {l,r,k,2,++qes}; } else { int pos,t; scanf("%d%d",&pos,&t); a[++tot]=(rec) {tmp[pos],-1,0,1,pos};//-1删除 tmp[pos]=t; a[++tot]=(rec) {tmp[pos],1,0,1,pos}; //+1增加 } } solve(1,tot,-inf,inf); fp(i,1,qes) printf("%d\n",ans[i]); return 0; }
整体二分的复杂度的话,
离散化优化整体二分 $O(m {log_2}^2 n)$
不优化(也差不多) $O(m {log_2} n log_2 INF)$