整体二分
(搞完这个专题就要开dp和数学了,诶诶)
前言
整体二分是一种离线处理时间轴的技术,对于特定问题可以做到 \(O(n\log^2 n)\) 复杂度(假设 \(n\) \(V\) 同阶),可惜常规写法复杂度有点大,不过经过优化常数较小。
简单分析一下思路,这类问题一般是将”操作/询问“离线处理,然后对答案二分(这就要求了答案具有某种单调性质),每层计算左区间答案所产生的贡献,以贡献为标准将“操作/询问”分类进入左右区间,最后类似于 \(CDQ\) 分治递归得到子答案。
接下来以几道题为例:
P3834 【模板】可持久化线段树 2
虽说是可持久化线段树的例题,但是我们不妨用整体二分来考虑一下:
我们记录数据下标,以权值为关键字排序,再离线询问,接着二分离散值域,每层计算贡献将询问分类,具体而言,就是从小到大对离散权值单点加,再对询问区间查询,如果满足当前询问排名,归入左区间进一步缩小范围,如果满足询问排名就进入右区间进一步缩小范围。最后记住消除当前层的贡献。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10;
int n,q;
int ans[N];
namespace BIT{#define lowbit(x) (x&(-x))int t[N];void add(int pos,int v){for(int i=pos;i<=N-10;i+=lowbit(i)) t[i]+=v;}int query(int pos){int res(0);for(int i=pos;i;i-=lowbit(i))res+=t[i];return res;}
}
struct Node{int l,r,k,pos;
}opt[N],lset[N],rset[N];
struct Point{int val,pos;bool operator<(const Point A)const{return val^A.val?val<A.val:pos<A.pos;}
}a[N];
void sol(int ql,int qr,int vl,int vr){if(ql>qr) return;if(vl==vr){for(int i=ql;i<=qr;++i) ans[opt[i].pos]=a[vl].val;return;}int mid((vl+vr)>>1),lz(0),rz(0);for(int i=vl;i<=mid;++i) BIT::add(a[i].pos,1);for(int i=ql;i<=qr;++i){int sas(BIT::query(opt[i].r)-BIT::query(opt[i].l-1));if(sas>=opt[i].k) lset[++lz]=opt[i];else opt[i].k-=sas,rset[++rz]=opt[i];}for(int i=1;i<=lz;++i) opt[ql+i-1]=lset[i];for(int i=1;i<=rz;++i) opt[ql+lz+i-1]=rset[i];for(int i=vl;i<=mid;++i) BIT::add(a[i].pos,-1);sol(ql,ql+lz-1,vl,mid);sol(ql+lz,qr,mid+1,vr);
}
signed main(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);cin>>n>>q;for(int i=1;i<=n;++i){cin>>a[i].val;a[i].pos=i;}for(int i=1;i<=q;++i){cin>>opt[i].l>>opt[i].r>>opt[i].k;opt[i].pos=i;}sort(a+1,a+n+1);sol(1,q,1,n);for(int i=1;i<=n;++i) cout<<ans[i]<<endl;}
显然对于静态区间第 \(k\) 小,这样的复杂度我们无法接受,我们来思考一下究竟是哪里造成了复杂度的浪费:每次对于序列状态的暴力刷新,因此我们考虑使用双指针维护这个前缀,可以消去一个 \(\log\)(不过这还是整体二分吗)。
P2617 Dynamic Rankings
区间带修改第 \(k\) 小。让我们来想想经典的主席树做法,为了维护“主席树前缀和”,我们使用了树状数组管理根节点,这是因为主席树本身就维护了一种”前缀和“,但是在这里我们显然不用考虑这些,因为我们在原始写法中本身就牺牲了一个 \(\log\) 的复杂度来离线处理单点,所以在操作时直接穿插询问和修改即可。
发现修改操作可以直接理解为从原数列中删去一个数再添加一个数,那么把操作放在二分过程中即可实现动态操作。
#include<bits/stdc++.h>
#define N 200001
#define lowbit(x) (x&(-x))
using namespace std;
int n,m,M,M2;
int a[N],ls[N<<1],siz,ans[N<<1],op2[N<<1];
int t[N<<1];
struct Q{int op,l,r,k,x,y,id;
}q[N<<1],q1[N<<1],q2[N<<1];
void upd(int x,int y){for(int i=x;i<=siz;i+=lowbit(i)) t[i]+=y;
}
int que(int x){int sum=0;for(int i=x;i>=1;i-=lowbit(i)) sum+=t[i];return sum;
}
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].op==2) ans[q[i].id]=l;return; }int mid=(l+r)/2,L=0,R=0;for(int i=ql;i<=qr;i++){if(!q[i].op){if(q[i].y<=mid) L++,q1[L]=q[i],upd(q[i].x,1);else R++,q2[R]=q[i];}else if(q[i].op==1){if(q[i].y<=mid) L++,q1[L]=q[i],upd(q[i].x,-1);else R++,q2[R]=q[i];}else{int nw=que(q[i].r)-que(q[i].l-1);if(nw>=q[i].k) L++,q1[L]=q[i];else R++,q2[R]=q[i],q2[R].k-= nw;}}for(int i=ql;i<=qr;i++){if(q[i].op==0&&q[i].y<=mid) upd(q[i].x,-1);if(q[i].op==1&&q[i].y<=mid) upd(q[i].x,1);}int i=ql;for(int j=1;j<=L;i++,j++) q[i]=q1[j];for(int j=1;j<=R;i++,j++) q[i]=q2[j];solve(l,mid,ql,ql+L-1),solve(mid+1,r,ql+L,qr);
}
int main(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);cin>>n>>m;for(int i=1;i<=n;i++){cin>>a[i];ls[++siz]=a[i];q[++M].id=M,q[M].op=0;q[M].x=i,q[M].y=a[i];}int l,r,k,x,y;char op;for(int i=1;i<=m;i++){cin>>op;if(op=='C'){cin>>x>>y;ls[++siz]=y;q[++M].op=1,q[M].x=x,q[M].y=a[x];q[++M].op=0,q[M].x=x;q[M].y=y,a[x]=y;}else{cin>>l>>r>>k;q[++M].op=2;q[M].id=++M2,q[M].l=l;q[M].r=r,q[M].k=k;}}sort(ls+1,ls+siz+1);siz=unique(ls+1,ls+siz+1)-ls-1;for(int i=1;i<=M;i++) if(!q[i].op||q[i].op) q[i].y=lower_bound(ls+1,ls+siz+1,q[i].y)-ls;solve(1,siz,1,M);for(int i=1;i<=M2;i++) cout<<ls[ans[i]]<<endl;
}
P1527 [国家集训队] 矩阵乘法
把树状数组换成二维的即可,几乎没有什么区别。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+10;
int n,q,m;
int ans[N];
namespace BIT{#define lowbit(x) (x&(-x))const int M=550;int t[M][M];void add(int posx,int posy,int v){for(int i=posx;i<=M-10;i+=lowbit(i)) for(int j=posy;j<=M-10;j+=lowbit(j))t[i][j]+=v;}int query(int posx,int posy){int res(0);for(int i=posx;i;i-=lowbit(i))for(int j=posy;j;j-=lowbit(j))res+=t[i][j];return res;}int qry(int x1,int y1,int x2,int y2){int s1,s2,s3,s4;s1=query(x2,y2),s2=query(x1-1,y1-1);s3=query(x1-1,y2),s4=query(x2,y1-1);return s1-s3-s4+s2;}
}
struct Node{int x1,y1,x2,y2,k,pos;
}opt[N],lset[N],rset[N];
struct Point{int val,posx,posy;bool operator<(const Point A)const{return val<A.val;}
}a[N];
void sol(int ql,int qr,int vl,int vr){if(ql>qr) return;if(vl==vr){for(int i=ql;i<=qr;++i) ans[opt[i].pos]=a[vl].val;return;}int mid((vl+vr)>>1),lz(0),rz(0);for(int i=vl;i<=mid;++i)BIT::add(a[i].posx,a[i].posy,1);for(int i=ql;i<=qr;++i){int sas=BIT::qry(opt[i].x1,opt[i].y1,opt[i].x2,opt[i].y2);if(sas>=opt[i].k) lset[++lz]=opt[i];else opt[i].k-=sas,rset[++rz]=opt[i];}for(int i=1;i<=lz;++i) opt[ql+i-1]=lset[i];for(int i=1;i<=rz;++i) opt[ql+i+lz-1]=rset[i];for(int i=vl;i<=mid;++i) BIT::add(a[i].posx,a[i].posy,-1);sol(ql,ql+lz-1,vl,mid);sol(ql+lz,qr,mid+1,vr);
}
signed main(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);cin>>n>>q;for(int i=1;i<=n;++i){for(int j=1;j<=n;++j){cin>>a[++m].val;a[m].posx=i;a[m].posy=j;}}for(int i=1;i<=q;++i){cin>>opt[i].x1>>opt[i].y1>>opt[i].x2>>opt[i].y2>>opt[i].k;opt[i].pos=i;}sort(a+1,a+m+1);sol(1,q,1,m);for(int i=1;i<=q;++i) cout<<ans[i]<<endl;
}
P3527 [POI 2011] MET-Meteors
首先经典的操作,二倍扩充环的长度破环成链,二分时间轴,也就是陨石,我们注意到这里每场陨石会对序列产生一个区间加值的操作。同时每层对国家进行分类,而每个国家应对其管辖的单点进行查询,因此治理需要一个维护区间加值,单点查询的数据结构,这里选用差分树状数组。整体时间复杂度 \(O(n\log^2 n)\)。
当然还有一些细节,比如存在无法达成目标的国家,所以我们提前创造一个保证可以达成目标的“超级陨石雨”,利用其进行可行性判断即可。其次,别忘记对链上的两个节点同时查询之和才是环上节点的真实数值。
#include<bits/stdc++.h>
#define int unsigned long long
#define inf LLONG_MAX
using namespace std;
const int N=6e5+10;
int n,m,k;
int ans[N];
int head[N],nxt[N],to[N];
struct Eve{int l,r,a,pos;
}e[N];
struct Con{int cnt,pos;
}con[N],lset[N],rset[N];
namespace BIT{#define lowbit(x) (x&(-x))int t[N];void add(int pos,int v){for(int i=pos;i<=N-10;i+=lowbit(i)) t[i]+=v;}int query(int pos){int res(0);for(int i=pos;i;i-=lowbit(i)) res+=t[i];return res;}
}
void add(int u,int v){to[++to[0]]=v,nxt[to[0]]=head[u],head[u]=to[0];
}
void sol(int ql,int qr,int vl,int vr){if(ql>qr) return;if(vl==vr){for(int i=ql;i<=qr;++i)ans[con[i].pos]=e[vl].pos;return;}int mid((vl+vr)>>1),lz(0),rz(0);for(int i=vl;i<=mid;++i){BIT::add(e[i].l,e[i].a);BIT::add(e[i].r+1,-e[i].a);}for(int i=ql;i<=qr;++i){int sas(0);for(int j=head[con[i].pos];j;j=nxt[j]){sas+=BIT::query(to[j]);sas+=BIT::query(to[j]+m);}if(sas>=con[i].cnt) lset[++lz]=con[i];else con[i].cnt-=sas,rset[++rz]=con[i];}for(int i=1;i<=lz;++i) con[ql+i-1]=lset[i];for(int i=1;i<=rz;++i) con[ql+i+lz-1]=rset[i];for(int i=vl;i<=mid;++i){BIT::add(e[i].l,-e[i].a);BIT::add(e[i].r+1,e[i].a);}sol(ql,ql+lz-1,vl,mid);sol(ql+lz,qr,mid+1,vr);
}
signed main(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);cin>>n>>m;for(int i=1,o;i<=m;++i)cin>>o,add(o,i);for(int i=1;i<=n;++i)cin>>con[i].cnt,con[i].pos=i;cin>>k;for(int i=1;i<=k;++i){cin>>e[i].l>>e[i].r>>e[i].a;if(e[i].l>e[i].r) e[i].r+=m;e[i].pos=i;}e[k+1]={1,m<<1,inf,k+1};sol(1,n,1,k+1);for(int i=1;i<=n;++i){if(ans[i]==k+1) cout<<"NIE"<<endl;else cout<<ans[i]<<endl;}
}