整体二分学习笔记
有一些题目需要用到二分,但多次询问直接二分,会导致 TLE,那么就需要用到一个离线算法,将多个询问放在一起二分,这就是整体二分。
条件
能够用整体二分解决的题目需要满足以下性质:
1.题目具有可二分性(即单调性);
2.修改对判定答案的贡献相互独立,修改之间互不影响效果。
3.修改如果对判定答案有贡献,则贡献为一个确定的,与判定标准无关的值;
4.贡献满足交换律,结合律,具有可加性;
5.题目允许使用离线算法。
方法
记 为答案的值域, 为答案的定义域。
- 首先将所有操作按时间顺序存入数组, 开始分治;
- 在每一层分治中,利用数据结构(一般是树状数组)统计当前查询的答案和 之间的关系;
- 根据查询出来的答案和 间的关系(小于等于 和大于 )将当前处理的操作序列分为 和 两部分,分别递归处理;
- 当 时,找到答案,记录答案并返回即可。
需要注意的是,在整体二分的过程中,若当前处理的值域为 ,则此时最终答案范围不在 的询问会在其他时候处理。
求全局第 小(多次询问)
可以对于每个询问进行一次二分;但是,也可以把所有的询问放在一起二分。
先考虑二分的本质:假设要猜一个 之间的数,猜测答案是 ,然后去验证 的正确性,再调整边界。这样做每次询问的复杂度为 ,若询问次数为 ,则时间复杂度为 。
回过头来,对于当前的所有询问,可以去猜测所有询问的答案都是 ,然后去依次验证每个询问的答案应该是小于等于 的还是大于 的,并将询问分为两个部分,对于每个部分继续二分。
注意:如果一个询问的答案是大于 的,则在将其划至右侧前需更新它的 ,即,如果当前数列中小于等于 的数有 个,则将询问划分后实际是在右区间询问第 小数。如果一个部分的 了,则结束这个部分的二分。利用线段树的相关知识,我们每次将整个答案可能在的区间 划分成了若干个部分,这样的划分共进行了 次,一次划分会将整个操作序列操作一次。若对整个序列进行操作,并支持对应的查询的时间复杂度为 ,则整体二分的时间复杂度为 。
求区间第 小(多次询问)
涉及到给定区间的查询,再按之前的方法进行二分就会导致 check
函数的时间复杂度爆炸。仍然考虑询问与值域中点 的关系:若询问区间内小于等于 的数有 个,询问的是区间内的 小数,则当 时,答案应小于等于 ;否则,答案应大于 。
此处需记录一个区间小于等于指定数的数的数量,即单点加,求区间和,可用树状数组快速处理。为提高效率,只对数列中值在值域区间 的数进行统计,即,在进一步递归之前,不仅将询问划分,将当前处理的数按值域范围划为两半。
例题 P3834【模板】可持久化线段树 2
板子题,求区间第 小。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int MAXN=4e5+5; const int INF=0x7f7f7f7f; struct Query { int l,r,k,id,type; }q[MAXN],q1[MAXN],q2[MAXN]; int t[MAXN],ans[MAXN]; int n,m,cnt; inline int lowbit(int x) {return x&-x;} inline void add(int x,int k) { for(int i=x;i<=MAXN;i+=lowbit(i)) t[i]+=k; return; } inline int ask(int x) { int res=0; for(int i=x;i>=1;i-=lowbit(i)) res+=t[i]; return res; } inline void solve(int l,int r,int ql,int qr) { if(ql>qr) return; if(l==r) { for(int i=ql;i<=qr;i++) if(q[i].type==2) ans[q[i].id]=l; return; } int mid=(l+r)>>1,cnt1=0,cnt2=0; for(int i=ql;i<=qr;i++) { if(q[i].type==1) { if(q[i].l<=mid) { add(q[i].id,1); q1[++cnt1]=q[i]; } else q2[++cnt2]=q[i]; } else { int res=ask(q[i].r)-ask(q[i].l-1); if(res>=q[i].k) q1[++cnt1]=q[i]; else q[i].k-=res,q2[++cnt2]=q[i]; } } for(int i=1;i<=cnt1;i++) if(q1[i].type==1) add(q1[i].id,-1); for(int i=1;i<=cnt1;i++) q[ql+i-1]=q1[i]; for(int i=1;i<=cnt2;i++) q[ql+i+cnt1-1]=q2[i]; solve(l,mid,ql,ql+cnt1-1),solve(mid+1,r,ql+cnt1,qr); return; } int main() { ios_base::sync_with_stdio(false); cin.tie(0),cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++) { int x; cin>>x; q[++cnt]={x,x,0,i,1}; } for(int i=1;i<=m;i++) { int l,r,k; cin>>l>>r>>k; q[++cnt]={l,r,k,i,2}; } solve(-INF,INF,1,cnt); for(int i=1;i<=m;i++) printf("%d\n",ans[i]); return 0; }
例题 P1527 [国家集训队] 矩阵乘法
一样是整体二分板子,不过是变成了矩阵,要用一下二维树状数组。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int MAXN=1e3+5; const int MAXQ=1e6+5; const int INF=0x7f7f7f7f; struct Query { int l1,r1,l2,r2,k,id; }q[MAXQ],q1[MAXQ],q2[MAXQ]; int t[MAXN][MAXN],ans[MAXQ]; int n,Q,cnt; inline int lowbit(int x) {return x&-x;} inline void add(int x,int y,int k) { for(int i=x;i<=n;i+=lowbit(i)) for(int j=y;j<=n;j+=lowbit(j)) t[i][j]+=k; return; } inline int ask(int x,int y) { int res=0; for(int i=x;i>=1;i-=lowbit(i)) for(int j=y;j>=1;j-=lowbit(j)) res+=t[i][j]; return res; } inline void solve(int l,int r,int ql,int qr) { if(ql>qr) return; if(l==r) { for(int i=ql;i<=qr;i++) if(q[i].id!=0) ans[q[i].id]=l; return; } int mid=(l+r)>>1,cnt1=0,cnt2=0; for(int i=ql;i<=qr;i++) { if(q[i].id==0) { if(q[i].k<=mid) { add(q[i].l1,q[i].r1,1); q1[++cnt1]=q[i]; } else q2[++cnt2]=q[i]; } else { int res=ask(q[i].l2,q[i].r2)-ask(q[i].l1-1,q[i].r2)-ask(q[i].l2,q[i].r1-1)+ask(q[i].l1-1,q[i].r1-1); if(res>=q[i].k) q1[++cnt1]=q[i]; else q[i].k-=res,q2[++cnt2]=q[i]; } } for(int i=1;i<=cnt1;i++) q[ql+i-1]=q1[i]; for(int i=1;i<=cnt2;i++) q[ql+i+cnt1-1]=q2[i]; for(int i=ql;i<=ql+cnt1-1;i++) if(q[i].id==0 && q[i].k<=mid) add(q[i].l1,q[i].r1,-1); solve(l,mid,ql,ql+cnt1-1),solve(mid+1,r,ql+cnt1,qr); return; } signed main() { ios_base::sync_with_stdio(false); cin.tie(0),cout.tie(0); cin>>n>>Q; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { int x; cin>>x; q[++cnt]={i,j,0,0,x,0}; } for(int i=1;i<=Q;i++) { int l1,r1,l2,r2,k; cin>>l1>>r1>>l2>>r2>>k; q[++cnt]={l1,r1,l2,r2,k,i}; } solve(-INF,INF,1,cnt); for(int i=1;i<=Q;i++) printf("%d\n",ans[i]); return 0; }
带修区间第 小(多次询问)
修改操作可以直接理解为从原数列中删去一个数再添加一个数,为方便起见,将询问和修改统称为「操作」。因后面的操作会依附于之前的操作,不能如题 3 一样将统计和处理询问分开,故可将所有操作存于一个数组,用标识区分类型,依次处理每个操作。为便于处理树状数组,修改操作可分拆为擦除操作和插入操作。
优化
-
注意到每次对于操作进行分类时,只会更改操作顺序,故可直接在原数组上操作。具体实现,在二分时将记录操作的 数组换为一个大的全局数组,二分时记录信息变为 ,即当前处理的操作是全局数组上的哪个区间。利用临时数组记录当前的分类情况,进一步递归前将临时数组信息写回原数组。
-
树状数组每次清空会导致时间复杂度爆炸,可采用每次使用树状数组时记录当前修改位置(这已由 1 中提到的临时数组实现),本次操作结束后在原位置加 的方法快速清零。
-
一开始对于数列的初始化操作可简化为插入操作。
例题 P2617 Dynamic Rankings
带上了修改,这道题也可以用树套树来解决。
树套树是要快一些的,但是整体二分要好写一些。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int MAXN=5e6+5; int n,m,cnt; int a[MAXN]; struct Query { int x,y,k,type,id; }q[MAXN],q1[MAXN],q2[MAXN]; int t[MAXN]; inline int lowbit(int x) {return x&-x;} inline void add(int x,int k) { for(int i=x;i<=n;i+=lowbit(i)) t[i]+=k; return; } inline int ask(int x) { int ans=0; for(int i=x;i>=1;i-=lowbit(i)) ans+=t[i]; return ans; } int ans[MAXN]; inline void solve(int l,int r,int ql,int qr) { if(ql>qr) return; int cnt1=0,cnt2=0,mid=(l+r)>>1; if(l==r) { for(int i=ql;i<=qr;i++) if(q[i].type==1) ans[q[i].id]=l; return; } for(int i=ql;i<=qr;i++) { if(q[i].type==1) { int now=ask(q[i].y)-ask(q[i].x-1); if(q[i].k<=now) q1[++cnt1]=q[i]; else q[i].k-=now,q2[++cnt2]=q[i]; } else { if(q[i].y<=mid) add(q[i].x,q[i].k),q1[++cnt1]=q[i]; else q2[++cnt2]=q[i]; } } for(int i=1;i<=cnt1;i++) if(q1[i].type==0) add(q1[i].x,-q1[i].k); for(int i=1;i<=cnt1;i++) q[ql+i-1]=q1[i]; for(int i=1;i<=cnt2;i++) q[ql+cnt1+i-1]=q2[i]; solve(l,mid,ql,ql+cnt1-1),solve(mid+1,r,ql+cnt1,qr); return; } int main() { ios_base::sync_with_stdio(false); cin.tie(0),cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++) { cin>>a[i]; q[++cnt]={i,a[i],1,0,0}; } for(int i=0;i<m;i++) { char op; int x; cin>>op>>x; if(op=='C') { int val; cin>>val; q[++cnt]={x,a[x],-1,0,0}; a[x]=val; q[++cnt]={x,val,1,0,0}; } else { int y,val; cin>>y>>val; q[++cnt]={x,y,val,1,i}; } } for(int i=0;i<m;i++) ans[i]=-1; solve(0,1e9,1,cnt); for(int i=0;i<m;i++) if(ans[i]!=-1) printf("%d\n",ans[i]); return 0; }
本文作者:Code_AC
本文链接:https://www.cnblogs.com/code-ac/p/17685163.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步