离线算法入门——整体二分

离线算法入门——整体二分

1 简介

整体二分是一类离线方法,适用于以下数据结构题:

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

简单来说,你发现这个题可以二分,但是如果直接对所有询问二分会超时,这个时候可以带着所有询问一起二分。

假设你现在二分出来的值为 (mid) ,你关注一下目前的询问集合 (q) ,检查集合里面的所有询问,对于询问值小于等于 (mid) 的,放在集合 (q_1) 里面,对于大于 (mid) 的,放在 (q_2) 里面,这样就把原问题划分成了两个子问题。这样说会很模糊,我们不妨结合例题来看。

2 例题 (1)

链接

很显然,随着时间的增加,落到一个国家上的陨石数量只增不减,所以具有可二分性,显然,修改之间互不影响结果,且与当前判定标准(也就是我们二分的值)无关,修改的贡献满足交换律,结合律,还有可加性。

这个题显然可以用整体二分来做。

我们如何来整体二分呢,我们来看代码分析。

2.1 代码分析

以下是所有定义。

int n,m,k,P[N],q[N],q1[N],q2[N],ans[N];

struct BIT{
    int p[N];
    inline int lowbit(int x){return x&(-x);}
    inline void add(int x,int val){for(int i=x;i<=m;i+=lowbit(i))p[i]+=val;}
    inline int ask_sum(int x){int ans=0;for(int i=x;i>=1;i-=lowbit(i)) ans+=p[i];return ans;}
};
BIT bit;

struct liuxingyu{
    int l,r,val;
}a[N];

vector<int> coun[N];

其中 (n,m,k) 如题目所示,(q,q_1,q_2) 和我们在整体二分中同名集合的意义一样,BIT 是树状数组,coun[N] 代表的是某个国家的空间站,具体来说 coun[i][j]=k 表示国家 (i) 有空间站 (k)

注意因为题目要求是环,所以我们复制一段接在末尾,断环为链。ans[i] 里存的是第 (i) 个询问的答案。

signed main(){
    // freopen("my.in","r",stdin);
    // freopen("my.out","w",stdout);
    read(n);read(m);
    for(int i=1;i<=m;i++){
        int x;read(x);
        coun[x].push_back(i);coun[x].push_back(i+m);
    }
    m+=m;
    for(int i=1;i<=n;i++) read(P[i]);
    read(k);
    for(int i=1;i<=k;i++){
        read(a[i].l);read(a[i].r);read(a[i].val);if(a[i].r<a[i].l) a[i].r+=(m/2);
    }
    for(int i=1;i<=n;i++) q[i]=i;
    k++;a[k].l=1;a[k].r=n;a[k].val=INF;
    solve(1,k,1,n);
    for(int i=1;i<=n;i++){
        if(ans[i]!=k) printf("%lld
",ans[i]);
        else printf("NIE
");
    }
    return 0;
}

这是主函数,其中 solve 是整体二分的过程,我们一会再说,注意题目需要让我们判断放不满的,我们就手动在加一场流星雨,让他一定能在第 (k+1) 场之前放满,这样就可以判断了。

接下来我们关注整体二分:

2.2 整体二分部分

inline void solve(int l,int r,int z,int y){
    if(l==r){
        for(int i=z;i<=y;i++) ans[q[i]]=l;return;
    }
    int mid=(l+r)>>1,tail1=0,tail2=0;
    for(int i=l;i<=mid;i++){
        bit.add(a[i].l,a[i].val);bit.add(a[i].r+1,-a[i].val);
    }
    for(int i=z;i<=y;i++){
        int now=0,len=coun[q[i]].size();
        for(int j=0;j<len;j++){
            now+=bit.ask_sum(coun[q[i]][j]);
            if(now>=P[q[i]]) break;
        }
        if(now>=P[q[i]]) q1[++tail1]=q[i];
        else{
            q2[++tail2]=q[i];P[q[i]]-=now;
        }
    }
    for(int i=l;i<=mid;i++) bit.add(a[i].l,-a[i].val),bit.add(a[i].r+1,a[i].val);
    for(int i=z;i<=z+tail1-1;i++) q[i]=q1[i-z+1];
    for(int i=z+tail1;i<=z+tail1+tail2-1;i++) q[i]=q2[i-z-tail1+1];
    solve(l,mid,z,z+tail1-1);solve(mid+1,r,z+tail1,z+tail1+tail2-1);
}

这个函数解决的是:答案区间在 (l,r) ,询问在 (z,y) 中的所有询问。注意询问之所以是连续的是因为我们在给询问分组之后又把它们放回了原数组。

注意我们强制落下 (l)(mid) 的流星雨,然后判断哪一些符合条件,从而进行分类。我们用树状数组来统计答案。

2.3 注意事项

  • (2.2) 中,需要注意当前的所有操作只能与当前的询问序列和值域有关,而不能与整个序列有关,否则,整体二分的复杂度将沦为暴力的复杂度。
  • (2.2) 中,如果达到要求要及时退出,否则会爆 long long
  • (2.2) 中,如果询问归到 (q_2) ,注意要让标准减去 (now) ,因为你不会在统计这边的答案了,要提前减去。详细点来说,就是分到 (q_2) 的所有询问,目前的 (now) 对他们的答案有贡献,要提前减去。

2.4 复杂度分析

这里就是整体二分的复杂度分析。先说如果在整体二分的过程中是对整个序列的操作,复杂度是什么?

下面的 (T(C,S)) 定义为当前值域为 (C) ,二分序列为 (S)

[T(C,S)=T(frac C2,S_0)+T(frac C2,S-S_0)+O(n)\ T(C,n)=O(nC) ]

整体二分的复杂度沦为暴力的复杂度。

如果是对当前序列的操作呢?

[T(C,S)=T(frac C2,S_0)+T(frac C2,S-S_0)+O(f(S))\ T(C,n)le O(f(n)log C) ]

复杂度可以接受。

这里放上如果与整个序列相关后的评测记录:链接

2.5 总代码

#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define int ll
#define uint unsigned int
#define ull unsigned long long
#define N 600010
#define M number
using namespace std;

const int INF=0x3f3f3f3f;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

int n,m,k,O[N],P[N],q[N],q1[N],q2[N],cou[N],ans[N];

struct BIT{
    int p[N];
    inline int lowbit(int x){return x&(-x);}
    inline void add(int x,int val){for(int i=x;i<=m;i+=lowbit(i))p[i]+=val;}
    inline int ask_sum(int x){int ans=0;for(int i=x;i>=1;i-=lowbit(i)) ans+=p[i];return ans;}
};
BIT bit;

struct liuxingyu{
    int l,r,val;
}a[N];

inline void solve(int l,int r,int z,int y){
    if(l==r){
        for(int i=z;i<=y;i++) ans[q[i]]=l;return;
    }
    int mid=(l+r)>>1,tail1=0,tail2=0;
    for(int i=l;i<=mid;i++){
        bit.add(a[i].l,a[i].val);bit.add(a[i].r+1,-a[i].val);
    }
    for(int i=1;i<=m;i++){
        int now=bit.ask_sum(i);
        cou[O[i]]+=now;
    }
    for(int i=z;i<=y;i++){
        if(cou[q[i]]>=P[q[i]]) q1[++tail1]=q[i];
        else{
            q2[++tail2]=q[i];P[q[i]]-=cou[q[i]];
        }
    }
    for(int i=1;i<=n;i++) cou[i]=0;
    for(int i=l;i<=mid;i++) bit.add(a[i].l,-a[i].val),bit.add(a[i].r+1,a[i].val);
    for(int i=z;i<=z+tail1-1;i++) q[i]=q1[i-z+1];
    for(int i=z+tail1;i<=z+tail1+tail2-1;i++) q[i]=q2[i-z-tail1+1];
    solve(l,mid,z,z+tail1-1);solve(mid+1,r,z+tail1,z+tail1+tail2-1);
}

signed main(){
    // freopen("my.in","r",stdin);
    // freopen("my.out","w",stdout);
    read(n);read(m);
    for(int i=1;i<=m;i++) read(O[i]),O[i+m]=O[i];m+=m;
    for(int i=1;i<=n;i++) read(P[i]);
    read(k);
    for(int i=1;i<=k;i++){
        read(a[i].l);read(a[i].r);read(a[i].val);if(a[i].r<a[i].l) a[i].r+=(m/2);
    }
    for(int i=1;i<=n;i++) q[i]=i;
    k++;a[k].l=1;a[k].r=n;a[k].val=INF;
    solve(1,k,1,n);
    for(int i=1;i<=n;i++){
        if(ans[i]!=k) printf("%lld
",ans[i]);
        else printf("NIE
");
    }
    return 0;
}

3 例题 (2)

链接

不难发现,这就是主席树擅长的区间第 (k) 小值。

但其实这个也可以用整体二分来做。

同样的,我们把一开始的序列看做多次插入来处理,这道题同时揭示了整体二分可以应对带加入的情况,我们只需要把加入和询问一起处理,一起分类就可以了。

#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 400010
#define M number
using namespace std;

const int INF=0x3f3f3f3f;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

struct operate{
    int l,r,val,id,type;
    inline operate(){}
    inline operate(int l,int r,int val,int id,int type) : l(l),r(r),val(val),id(id),type(type) {}
};
operate q[N<<1],q1[N<<1],q2[N<<1];

int n,m,a[N],tail,ans[N],rk[N],c[N],tail1,tail2;

struct BIT{
    int p[N];
    inline int lowbit(int x){return x&(-x);}
    inline void add(int x,int val){
        for(int i=x;i<=n;i+=lowbit(i)) p[i]+=val;
    }
    inline int ask_sum(int x){
        int ans=0;
        for(int i=x;i>=1;i-=lowbit(i)) ans+=p[i];
        return ans;
    }
};
BIT bit;

inline void solve(int l,int r,int z,int y){
    if(l==r){
        for(int i=z;i<=y;i++) if(q[i].type==2) ans[q[i].id]=rk[l];
        return;
    }
    int mid=(l+r)>>1;int tail1=0,tail2=0;
    for(int i=z;i<=y;i++){
        if(q[i].type==1){
            if(q[i].val<=mid){
                bit.add(q[i].id,1);
                q1[++tail1]=q[i];
            }else q2[++tail2]=q[i];
        }
        else{
            int nowans=bit.ask_sum(q[i].r)-bit.ask_sum(q[i].l-1);
            if(nowans>=q[i].val) q1[++tail1]=q[i];
            else{q2[++tail2]=q[i];q2[tail2].val-=nowans;}
        }
    }
    for(int i=z;i<=y;i++) if(q[i].val<=mid&&q[i].type==1) bit.add(q[i].id,-1);
    for(int i=z;i<=z+tail1-1;i++)
        q[i]=q1[i-z+1];
    for(int i=z+tail1;i<=z+tail1+tail2-1;i++)
        q[i]=q2[i-z-tail1+1];
    solve(l,mid,z,z+tail1-1);
    solve(mid+1,r,z+tail1,z+tail1+tail2-1);  
}

inline void lisanhua(){
    for(int i=1;i<=n;i++) c[i]=a[i];
    sort(c+1,c+n+1);int w=unique(c+1,c+n+1)-c-1;
    for(int i=1;i<=n;i++){
        int last=a[i];
        a[i]=lower_bound(c+1,c+w+1,a[i])-c;
        rk[a[i]]=last;
    }
}

int main(){
    // freopen("my.in","r",stdin);
    // freopen("my.out","w",stdout);
    read(n);read(m);for(int i=1;i<=n;i++) read(a[i]);
    lisanhua();
    for(int i=1;i<=n;i++){q[++tail]=operate(-1,-1,a[i],i,1);}
    for(int i=1;i<=m;i++){
        int l,r,k;read(l);read(r);read(k);
        q[++tail]=operate(l,r,k,i,2);
    }
    solve(1,n,1,tail);
    for(int i=1;i<=m;i++) printf("%d
",ans[i]);
    return 0;
}

4 总结

整体二分其实还可以支持带修,不过那个我们再说。

事实上,整体二分就是在把一些操作进行分类,对加入分类,对询问分类,看看答案的取值范围在哪里,从而将原问题划分成若干子问题来求解。在可以离线的时候,离线算法并不比在线算法差,它们往往因为拥有更多的信息在面对许多问题是游刃有余。

5 引用

  • 《浅谈数据结构题的几个非经典解法》—— 许昊然
原文地址:https://www.cnblogs.com/TianMeng-hyl/p/14977002.html