整体二分

2021/10/28 23:15:36

本文主要是介绍整体二分,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

整体二分:

引入:

有些题目需要二分,但是当有多次询问二分有可能 \(T\) 飞,这个时候就用到了 整体二分

定义:

整体二分就是:多个查询一起通过二分解决,也就是和莫队一样的离线算法。

性质:

  1. 询问的答案具有可二分性。
  2. 修改对判定答案的贡献互相独立,修改之间互不影响效果。
  3. 修改如果对判定答案有贡献,则贡献为确定的判定标准无关的值。
  4. 贡献满足交换律,结合律,具有可加性。

思路:

记 \([l,r]\) 为答案的值域,\([L,R]\) 为答案的定义域(下标在 \([L,R]\) 之间)

  1. 首先把所有操作按照时间顺序存入数组中,然后开始分治。
  2. 在每一层分治中,利用数据结构统计当前查询的答案和 \(mid\) 之间的关系(感觉有点像 \(CDQ\) 分治)
  3. 根据查询出来的答案和 \(mid\) 之间的关系,将当前处理的操作序列分成 \(q_1\) 和 \(q_2\) 两份,并且递归。
  4. 当 \(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\) 小数:

  1. \(k \leq res\) ,答案小于等于 \(mid\)。
  2. \(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\) 小。

分析:

为了方便,将询问和修改统称为操作。因为后面的操作会依赖于前面的操作,因此可以将所有操作存在一个数组里,使用 标记 区分类型。

修改操作可以理解成:从原数列中删除一个数再添加一个数

因此,可转化为:树状数组中删除之前的数,再插入现在的数。

考虑优化:

  1. 每次对操作进行分类,只会更改操作顺序,因此用 全局数组 记录每次的操作,同时,二分时记录信息变成 \([L,R]\) ,也就是处理的操作区间。
    利用临时数组记录分类情况,然后递归前再放回原数组中(顺序改变)

  2. 数列的初始化操作可简化为插入操作。

代码:

// 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;
}


这篇关于整体二分的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程