整体二分学习笔记

发布时间 2023-09-07 15:54:13作者: Code_AC

有一些题目需要用到二分,但多次询问直接二分,会导致 TLE,那么就需要用到一个离线算法,将多个询问放在一起二分,这就是整体二分。

条件

能够用整体二分解决的题目需要满足以下性质:
1.题目具有可二分性(即单调性);
2.修改对判定答案的贡献相互独立,修改之间互不影响效果。
3.修改如果对判定答案有贡献,则贡献为一个确定的,与判定标准无关的值;
4.贡献满足交换律,结合律,具有可加性;
5.题目允许使用离线算法。

方法

\([l,r]\) 为答案的值域,\([L,R]\) 为答案的定义域。

  • 首先将所有操作按时间顺序存入数组, 开始分治;
  • 在每一层分治中,利用数据结构(一般是树状数组)统计当前查询的答案和 \(mid\) 之间的关系;
  • 根据查询出来的答案和 \(mid\) 间的关系(小于等于 \(mid\) 和大于 \(mid\))将当前处理的操作序列分为 \(q1\)\(q2\) 两部分,分别递归处理;
  • \(l=r\) 时,找到答案,记录答案并返回即可。
    需要注意的是,在整体二分的过程中,若当前处理的值域为 \([l,r]\),则此时最终答案范围不在 \([l,r]\) 的询问会在其他时候处理。

求全局第 \(k\) 小(多次询问)

可以对于每个询问进行一次二分;但是,也可以把所有的询问放在一起二分。

先考虑二分的本质:假设要猜一个 \([l,r]\) 之间的数,猜测答案是 \(m = \lfloor\frac{l + r}{2}\rfloor\),然后去验证 \(m\) 的正确性,再调整边界。这样做每次询问的复杂度为 \(\Theta(\log n)\),若询问次数为 \(q\),则时间复杂度为 \(\Theta(q\log n)\)

回过头来,对于当前的所有询问,可以去猜测所有询问的答案都是 \(mid\),然后去依次验证每个询问的答案应该是小于等于 \(mid\) 的还是大于 \(mid\) 的,并将询问分为两个部分,对于每个部分继续二分。

注意:如果一个询问的答案是大于 \(mid\) 的,则在将其划至右侧前需更新它的 \(k\),即,如果当前数列中小于等于 \(mid\) 的数有 \(t\) 个,则将询问划分后实际是在右区间询问第 \(k - t\) 小数。如果一个部分的 \(l = r\) 了,则结束这个部分的二分。利用线段树的相关知识,我们每次将整个答案可能在的区间 \([1,maxans]\) 划分成了若干个部分,这样的划分共进行了 \(\Theta(\log maxans)\) 次,一次划分会将整个操作序列操作一次。若对整个序列进行操作,并支持对应的查询的时间复杂度为 \(\Theta(T)\),则整体二分的时间复杂度为 \(\Theta(T\log n)\)

求区间第 \(k\) 小(多次询问)

涉及到给定区间的查询,再按之前的方法进行二分就会导致 check 函数的时间复杂度爆炸。仍然考虑询问与值域中点 \(m\) 的关系:若询问区间内小于等于 \(m\) 的数有 \(t\) 个,询问的是区间内的 \(k\) 小数,则当 \(k \leqslant t\) 时,答案应小于等于 \(m\);否则,答案应大于 \(m\)

此处需记录一个区间小于等于指定数的数的数量,即单点加,求区间和,可用树状数组快速处理。为提高效率,只对数列中值在值域区间 \([l,r]\) 的数进行统计,即,在进一步递归之前,不仅将询问划分,将当前处理的数按值域范围划为两半。

例题 P3834【模板】可持久化线段树 2

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=4e5+5;
const int INF=0x7f7f7f7f;

struct Query
{
    int l,r,k,id,type;
}q[MAXN],q1[MAXN],q2[MAXN];

int t[MAXN],ans[MAXN];
int n,m,cnt;

inline int lowbit(int x) {return x&-x;}

inline void add(int x,int k)
{
    for(int i=x;i<=MAXN;i+=lowbit(i)) t[i]+=k;
    return;
}

inline int ask(int x)
{
    int res=0;
    for(int i=x;i>=1;i-=lowbit(i)) res+=t[i];
    return res;
}

inline 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].type==2) ans[q[i].id]=l;
        return;
    }
    int mid=(l+r)>>1,cnt1=0,cnt2=0;
    for(int i=ql;i<=qr;i++)
    {
        if(q[i].type==1)
        {
            if(q[i].l<=mid)
            {
                add(q[i].id,1);
                q1[++cnt1]=q[i];
            }
            else q2[++cnt2]=q[i];
        }
        else
        {
            int res=ask(q[i].r)-ask(q[i].l-1);
            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].type==1) add(q1[i].id,-1);
    for(int i=1;i<=cnt1;i++) q[ql+i-1]=q1[i];
    for(int i=1;i<=cnt2;i++) q[ql+i+cnt1-1]=q2[i];
    solve(l,mid,ql,ql+cnt1-1),solve(mid+1,r,ql+cnt1,qr);
    return;
}


int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        int x; cin>>x;
        q[++cnt]={x,x,0,i,1};
    }
    for(int i=1;i<=m;i++)
    {
        int l,r,k; cin>>l>>r>>k;
        q[++cnt]={l,r,k,i,2};
    }
    solve(-INF,INF,1,cnt);
    for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
    return 0;
}