整体二分
2021/10/28 23:15:36
本文主要是介绍整体二分,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
整体二分:
引入:
有些题目需要二分,但是当有多次询问二分有可能 \(T\) 飞,这个时候就用到了 整体二分
定义:
整体二分就是:多个查询一起通过二分解决,也就是和莫队一样的离线算法。
性质:
- 询问的答案具有可二分性。
- 修改对判定答案的贡献互相独立,修改之间互不影响效果。
- 修改如果对判定答案有贡献,则贡献为确定的判定标准无关的值。
- 贡献满足交换律,结合律,具有可加性。
思路:
记 \([l,r]\) 为答案的值域,\([L,R]\) 为答案的定义域(下标在 \([L,R]\) 之间)
- 首先把所有操作按照时间顺序存入数组中,然后开始分治。
- 在每一层分治中,利用数据结构统计当前查询的答案和 \(mid\) 之间的关系(感觉有点像 \(CDQ\) 分治)
- 根据查询出来的答案和 \(mid\) 之间的关系,将当前处理的操作序列分成 \(q_1\) 和 \(q_2\) 两份,并且递归。
- 当 \(l=r\) ,记录答案返回即可。
例子:
\(Q\): 在一个数列中多次查询第 \(k\) 小的数
\(A\):
我们假设当前所有询问的答案都是 \(mid\),然后枚举判断小于 \(mid\) 还是大于 \(mid\) :
-
如果小于 \(mid\) ,那么将这些询问继续向 \([l,mid]\) 搜, \(k\) 不变
-
如果大于 \(mid\) , 那么将这些询问向 \([mid+1,r]\) 搜, \(k=k-mid\) ,说明在右边这个区间里,整个序列第 \(k\) 大的数是这个区间里第 \(k-mid\) 大的数。
如果等于 \(mid\) , 通过序号直接记录答案即可。
时间复杂度为 \(O(T \log n)\)
代码:
struct Query{ int id,k; }; int ans[N]; int check(int x){...}//返回原数列中小于等于x的数 void solve(int l,int r,vector<Query> q){ int mid=l+r>>1; if(l==r){ for(int i=0;i<q.size();i++) ans[q[i].id]=l; return ; } vector<int> q1,q2; for(int i=0;i<q.size();i++) if(q[i].k<=check(mid)) q1.push_back(q[i]); else q[i].k-=check(mid),q2.push_back(q[i]); solve(l,mid,q1); solve(mid+1,r,q2); }
\(Q:\) 在一个数列中多次查询区间第 \(k\) 小的值:
\(A:\)
涉及到给定区间的查询,再按之前的方法进行二分就会时间复杂度爆炸。
考虑询问和值域中点 \(mid\) 的关系:
询问区间内小于等于 \(mid\) 的数有 \(res\) 个,询问点是区间内的 \(k\) 小数:
- \(k \leq res\) ,答案小于等于 \(mid\)。
- \(k> res\) ,答案大于 \(mid\) , \(k=k-res\)。
此处需要记录一个区间小于等于指定数的结构,即树状数组。
为了提高效率,只对数列中值在区间 \([l,r]\) 的数进行统计。
下一步递归时,不仅将询问划分,还要将当前处理的数按值域划成两半。
代码:
struct Num{ int p,x; }//位于数列中第 p 项的数的值为x struct Query{ int l,r,k,id; };//编号为id,询问 [l,r] 中第 k 小数的询问 void add(int x,int c){...} int query(int x){...} void solve(int l,int r,vector<Num> a,vector<Query> q){ int mid=l+r>>1; if(l==r) {for(int i=0;i<q.size();i++) ans[q[i].id]=l; return;} vector<Num>a1,a2; vector<Query>q1,q2; for(int i=0;i<a.size();i++){ if(a[i].x<=mid) a1.push_back(a[i]),add(a[i].p,1); else a2.push_back(a[i]); } for(int i=0;i<q.size();i++){ int res=query(q[i].r)-query(q[i].l-1); if(q[i].k<=res) q1.push_back(q[i]); else q[i].k-=res,q2.push_back(q[i]); } for(int i=0;i<a.size();i++) if(a[i].x<=mid) add(a[i].p,-1); solve(l,mid,a1,q1); solve(mid+1,r,a2,q2); }
\(Q:\) 带修区间第 \(k\) 小:
\(A:\)
这个是整体二分的完整运用。给一道例题:
P2617 Dynamic Rankings
题意:
给定一个序列,要求支持单点修改和区间查第 \(k\) 小。
分析:
为了方便,将询问和修改统称为操作。因为后面的操作会依赖于前面的操作,因此可以将所有操作存在一个数组里,使用 标记 区分类型。
修改操作可以理解成:从原数列中删除一个数再添加一个数。
因此,可转化为:树状数组中删除之前的数,再插入现在的数。
考虑优化:
-
每次对操作进行分类,只会更改操作顺序,因此用 全局数组 记录每次的操作,同时,二分时记录信息变成 \([L,R]\) ,也就是处理的操作区间。
利用临时数组记录分类情况,然后递归前再放回原数组中(顺序改变) -
数列的初始化操作可简化为插入操作。
代码:
// P2617 Dynamic Rankings #include<bits/stdc++.h> #define lowbit(x) x&-x using namespace std; const int N=2e6+5,inf=1e9+7; int n,m; int a[N],ans[N],c[N],cnt; struct node{ int x,y,k,id,op; }q[N],q1[N],q2[N]; //对于询问,op=1,[x,y]表示左右边界; op=2,x表示修改位置,y表示修改后的值 //k表示当前操作是插入1还是擦除-1 //id记录每个操作原先的编号 void add(int x,int z){ for(;x<=n;x+=lowbit(x)) c[x]+=z; } int query(int x){ int res=0; for(;x;x-=lowbit(x)) res+=c[x]; return res; } void solve(int L,int R,int l,int r){//操作域和值域 if(l>r||L>R) return; if(l==r){ for(int i=L;i<=R;i++) if(q[i].op==2) ans[q[i].id]=l; return; } int cnt1=0,cnt2=0,mid=l+r>>1;//分到左边,右边的操作数 for(int i=L;i<=R;i++){ if(q[i].op!=2){//是修改,就要更新树状数组 if(q[i].x<=mid) add(q[i].id,q[i].op),q1[++cnt1]=q[i];//值域在左边,进行添加 else q2[++cnt2]=q[i]; } else{//是询问,进行分类 int res=query(q[i].y)-query(q[i].x-1);//查询[l,mid]之间点的个数 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].op!=2) add(q1[i].id,-q1[i].op);//删除树状数组做出的贡献 for(int i=1;i<=cnt1;i++) q[i+L-1]=q1[i]; for(int i=1;i<=cnt2;i++) q[i+L+cnt1-1]=q2[i]; solve(L,L+cnt1-1,l,mid); solve(L+cnt1,R,mid+1,r); } int main(){ cin>>n>>m; for(int i=1;i<=n;i++){ scanf("%d",&a[i]); q[++cnt]=node{a[i],0,0,i,1}; } int num=0; for(int i=1;i<=m;i++){ char ch[5]; scanf("%s",ch); if(ch[0]=='Q'){ int l,r,k; scanf("%d%d%d",&l,&r,&k); q[++cnt]=node{l,r,k,++num,2}; } else{ int x,k; scanf("%d%d",&x,&k); q[++cnt]=node{a[x],0,0,x,-1}; q[++cnt]=node{a[x]=k,0,0,x,1}; } } solve(1,cnt,0,inf); for(int i=1;i<=num;i++){ printf("%d\n",ans[i]); } system("pause"); return 0; }
多余例题:
P1527 [国家集训队]矩阵乘法
这个题就是二维的找区间第 \(k\) 小值。
把树状数组改成二维即可。
同时还有一些细节注意处理。
P3527 [POI2011]MET-Meteors
这个题看着和上面两题有些不一样,但是其实是一个东西:
因为有跨越 \(n\) 的操作,这些可以用 树状数组 的区间改变值来处理。
输入和加值都可以当成操作,像之前处理就行。
同时还要注意这题的限制空间和时间,这种方法都是正好卡过的...
// P3527 [POI2011]MET-Meteors #include<bits/stdc++.h> using namespace std; #define ll unsigned long long #define lowbit(x) x&-x const int N=6e5+5; int n,m,ans[N],cnt,mb[N],k; ll c[N]; struct node{ int l,r,op,id;ll k; }q[N],q1[N],q2[N]; vector<int> e[N]; inline int read(){ int k=0,f=1;char c=getchar(); for(;!isdigit(c);c=getchar()) if(c=='-')f=-1; for(;isdigit(c);c=getchar()) k=k*10+c-'0';return f*k; } void add(int x,ll z){ for(;x<=m;x+=lowbit(x)) c[x]+=z; } int query(int x){ int res=0; for(;x;x-=lowbit(x)) res+=c[x]; return res; } void solve(int L,int R,int l,int r){ // cout<<L<<" "<<R<<" "<<l<<" "<<r<<endl; if(L>R||l>r) return; if(l==r){ for(int i=L;i<=R;i++) if(q[i].op==1) ans[q[i].id]=l; return; } int cnt1=0,cnt2=0,mid=l+r>>1; for(int i=L;i<=R;i++){ if(q[i].op==1){ ll res=0; for(int j=0;j<e[q[i].id].size();j++){ res+=query(e[q[i].id][j]); if(res>q[i].k) break;//超过目标就终止 } if(res>=q[i].k) q1[++cnt1]=q[i]; else q[i].k-=res,q2[++cnt2]=q[i]; } else{ if(q[i].id<=mid){ if(q[i].op==2) add(q[i].l,q[i].k),add(q[i].r+1,-q[i].k); else add(1,q[i].k),add(q[i].r+1,-q[i].k),add(q[i].l,q[i].k); q1[++cnt1]=q[i]; } else q2[++cnt2]=q[i]; } } for(int i=L;i<=R;i++){ if(q[i].op!=1&&q[i].id<=mid){ if(q[i].op==2) add(q[i].l,-q[i].k),add(q[i].r+1,q[i].k); else add(1,-q[i].k),add(q[i].r+1,q[i].k),add(q[i].l,-q[i].k); } } int flag1=0,flag2=0; for(int i=1;i<=cnt1;i++) q[i+L-1]=q1[i],flag1=1; for(int i=1;i<=cnt2;i++) q[i+L+cnt1-1]=q2[i],flag2=1; if(flag1) solve(L,L+cnt1-1,l,mid); if(flag2) solve(L+cnt1,R,mid+1,r); } int main(){ cin>>n>>m; for(int i=1,x;i<=m;i++){ x=read(); e[x].push_back(i); } for(int i=1;i<=n;i++){ mb[i]=read(); } cin>>k; for(int i=1;i<=k;i++){ q[i].l=read(); q[i].r=read(); q[i].k=read(); // scanf("%d%d%lld",&q[i].l,&q[i].r,&q[i].k); if(q[i].r>=q[i].l) q[i].op=2; //没有跨越 else q[i].op=3; q[i].id=i; //跨越 } for(int i=1;i<=n;i++) q[i+k].k=mb[i],q[i+k].op=1,q[i+k].id=i; // cout<<1<<endl; solve(1,k+n,1,k+1); for(int i=1;i<=n;i++){ if(ans[i]!=k+1) printf("%d\n",ans[i]); else puts("NIE"); } // system("pause"); return 0; }
这篇关于整体二分的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南