整体二分
整体二分:
引入:
有些题目需要二分,但是当有多次询问二分有可能 \(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:\)
这个是整体二分的完整运用。给一道例题:
题意:
给定一个序列,要求支持单点修改和区间查第 \(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;
}
多余例题:
这个题就是二维的找区间第 \(k\) 小值。
把树状数组改成二维即可。
同时还有一些细节注意处理。
这个题看着和上面两题有些不一样,但是其实是一个东西:
因为有跨越 \(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;
}