zoukankan      html  css  js  c++  java
  • 二分答案 / 整体二分学习笔记

    本讲内容分为两个部分;

    第1部分:二分答案介绍(淦,不是讲PJ算法)

    第2部分:整体二分介绍(我们是讲整体二分)

    Part 1 二分答案介绍

    相信大家都对二分答案比较熟悉。

    至少...需要会二分查找...

    我们考虑对于决策单调的问题:

    给出一个长度为n的递增数列,现在有m个询问,每个询问有一个参数$k_i$

    对于每一个询问输出这个递增序列中的大于等于$k_i$的位置最靠前的项的位置。

    如果没有输出$-1$

    对于100%的数据$ n,m leq 10^6$

    这个就大概是二分查找的模板题了,我们当考虑的是当前搜区间是$[L,R]$

    其中间项$M=left lfloor frac{L+R}{2} ight floor$大于还是小于$k_i$

    如果严格$a_M geq k_i$,那么搜$[L,M]$否则搜$[M+1,R]$

    显然这段代码就可以干这件事情

    # include <bits/stdc++.h>
    using namespace std;
    const int N=1e6+10;
    int a[N],n,m;
    int work(int k)
    {
        int l=1,r=n,ans=-1;
        while (l<=r) {
            int m=(l+r)/2;
            if (a[m]>=k) r=m-1,ans=m;
            else l=m+1;
        }
        return ans;
    }
    int main()
    {
        scanf("%d%d",&n,&m);
        for (int i=1;i<=n;i++) scanf("%d",&a[i]);
        while (m--) {
            int k; scanf("%d",&k);
            printf("%d
    ",work(k));
        }
        return 0;
    }
    二分查找lower_bound()实现

    不作赘述。

    这里写图片描述

    满足决策单调性的事情可以用二分答案转化为判定类问题。

    给出二分答案模板:

    int l=-inf,r=inf,ans;
    while (l<=r) {
        int mid=(l+r)>>1;
        if (check(mid)) ans=mid,l=mid+1; //向更优解尝试
        else r=mid-1; //向更劣解尝试
    }
    return ans;

    然而今天的重点不是这个,这个有点low

    Part 2 整体二分介绍

    看完上面二分答案是不是感觉这篇blog在水?

    其实没有,整体二分是二分答案的加强版更加优美、便捷、不易打挂。

    其实本质就是对于操作区间$[ql,qr]$,保证其答案在$[L,R]$中,然后通过$[L,R]$的二分然后逐步缩小,直到L=R,求出$[ql,qr]$的答案是L

    上面讨论了一个最显然的问题,考虑如果在上述定义下$[ql,qr]$,保证其答案在$[L,R]$中,当$L=R$时有,$[ql,qr]$答案都为L

    对于每一个属于[ql,qr]的询问,考虑当前答案$M=left lfloor frac{L+R}{2} ight floor$是否对其有贡献,如果有累加贡献。

    然后对于每一个属于[ql,qr]的询问,考虑之前累加贡献+新增贡献期望贡献值比较,

    如果期望贡献值 >= 累加贡献+新增贡献 那么答案就需要比M要大or等(这里按照正相关)

    如果期望贡献值 <= 累加贡献+新增贡献 那么答案就需要比M要小or等(这里按照正相关)

    然后回退此次操作的更新数据结构的值,保证复杂度只和qr-ql+1有关

    最后期望答案小于等于M的和期望答案大于M的分别分治solve即可。

    还记得上次[可持久化数据结构学习笔记]中学习的主席树吗?

    题目1 : 给出模板题目: P3834 【模板】可持久化线段树 1(主席树)

    我们使用树状数组维护当前的贡献[本质是维护位置,值才是贡献],这里的累加贡献和新增贡献就是[x,y]树状数组维护的区间和

    这里的期望贡献值就是K。

    主要是循环里面讨论的问题:

    1.是更改操作[pos,x]

        如果 更改位置上的值x > M 那么对贡献无影响,放到q2里面 , 更大的才会用到他

      如果 更改位置上的值x <=M 那么对答案有影响,放到q1里面,已经加入更大的已经加过了[表现为减去...],在树状数组维护的相应位置上pos + 1

    2.是查询操作[l,r,k]

      设w为 查询 [l,r]树状数组维护有w个答案在此下标范围内。

      如果 w <= k 那么答案应该比这个M更小或等 , 放到q1里面   

      如果 w>k    那么答案应该比M更大 , 减去当前查询已知有这么多[更大加过的表现],放到q2里面,查询更大答案。

    代码Code:请结合上述注释食用

    # include <bits/stdc++.h>
    # define inf (0x7f7f7f7f)
    using namespace std;
    const int N=2e5+10;
    struct rec{int l,r,k,id,type,val;}a[N<<1],q1[N<<1],q2[N<<1];
    int ans[N<<1],n,m,tot;
    # define lowbit(x) (x&(-x))
    int c[N];
    inline int read()
    {
        int X=0,w=0; char c=0;
        while(c<'0'||c>'9') {w|=c=='-';c=getchar();}
        while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar();
        return w?-X:X;
    }
    inline void write(int x)
    {
        if (x<0) { x=-x; putchar('-');}
        if (x>9) write(x/10);
        putchar('0'+x%10);
    }
    void update(int x,int y){for (;x<=n;x+=lowbit(x)) c[x]+=y;}
    int query(int x){int ret=0;for (;x;x-=lowbit(x)) ret+=c[x];return ret;}
    # undef lowbit
    void solve(int ql,int qr,int L,int R)
    {
        if (ql>qr) return;
        if (L==R) {
            for (int i=ql;i<=qr;i++)
             if (a[i].type==2) ans[a[i].id]=L;
            return;
        }
        int M=(L+R)>>1,t1=0,t2=0;
        for (int i=ql;i<=qr;i++)
         if (a[i].type==1) {
             if (a[i].val<=M) update(a[i].id,1),q1[++t1]=a[i];
             else q2[++t2]=a[i];
         } else if (a[i].type==2) {
             int num=query(a[i].r)-query(a[i].l-1);
             if (num>=a[i].k) q1[++t1]=a[i];
             else a[i].k-=num,q2[++t2]=a[i];
         }
         for (int i=1;i<=t1;i++)
          if (q1[i].type==1) update(q1[i].id,-1);
         for (int i=1;i<=t1;i++) a[ql+i-1]=q1[i];
         for (int i=1;i<=t2;i++) a[ql+t1+i-1]=q2[i];
         solve(ql,ql+t1-1,L,M);
         solve(ql+t1,qr,M+1,R);
    }
    int main()
    {
        n=read();m=read();
        for (int i=1;i<=n;i++) {
            int t=read();
            a[++tot].id=tot;
            a[tot].type=1;
            a[tot].val=t;
        }
        for (int i=1;i<=m;i++) {
            a[++tot].type=2;a[tot].l=read();
            a[tot].r=read();a[tot].k=read();a[tot].id=tot;
        }
        solve(1,tot,-inf,inf);
        for (int i=n+1;i<=tot;i++) write(ans[i]),putchar('
    ');
        return 0;
    }
    静态区间K大数 整体二分算法

    题目2: P2617 Dynamic Rankings 

    这个树状数组套可持久化线段树还记不记得,其实用整体二分是很简单实现的。

    就是把删除操作拆分成2个操作,删(减去原来的)加(加上现在的)

    我们把一定部分的代码做出了修改,主要是在树状数组加入(+y)和删除(-y)上

    对于每一个2操作多了一个参数y表示是删除(-1)还是加上(1)

    代码Code:请结合上述题目1和上述注释食用:

    # include <bits/stdc++.h>
    # define fp(i,s,t) for(int i=s;i<=t;i++)
    using namespace std;
    const int N=(1e5+5)*3;
    const int inf=1e9;
    struct rec{
        int x,y,k,op,id;
        //如果是1操作 值,加/减,无,操作编号,答案编号
        //如果是2操作 左边界,右边界,k小值,操作编号,数组中位置编号
    }a[N],q1[N],q2[N];
    int tmp[N],ans[N],n,m,tot;
    # define lowbit(x) (x&(-x))
    int c[N];
    void update(int x,int y) { for (;x<=n;x+=lowbit(x)) c[x]+=y;}
    int query(int x) {int ret=0;for (;x;x-=lowbit(x)) ret+=c[x];return ret;}
    # undef lowbit
    void solve(int ql,int qr,int L,int R)
    {
        if (ql>qr) return;
        if (L==R) {
            fp(i,ql,qr) if (a[i].op==2) ans[a[i].id]=L;
            return;
        }
        int M=(L+R)>>1,t1=0,t2=0;
        fp(i,ql,qr)
         if (a[i].op==1) {
             if (a[i].x<=M) {
                 update(a[i].id,a[i].y); //*加上a[i].y
                 q1[++t1]=a[i];
            } else q2[++t2]=a[i];
         } else {
             int num=query(a[i].y)-query(a[i].x-1);
             if (num>=a[i].k) q1[++t1]=a[i];
             else a[i].k-=num,q2[++t2]=a[i];
         }
         for (int i=1;i<=t1;i++)
          if (q1[i].op==1) update(q1[i].id,-q1[i].y);//*加上-a[i].y还原
         fp(i,1,t1) a[ql+i-1]=q1[i];
         fp(i,1,t2) a[ql+t1+i-1]=q2[i];
         solve(ql,ql+t1-1,L,M);
         solve(ql+t1,qr,M+1,R);
    }
    int main()
    {
        scanf("%d%d",&n,&m);
        fp(i,1,n) {
            scanf("%d",&tmp[i]);
            a[++tot]=(rec) {tmp[i],1,0,1,i};
        }
        int qes=0;
        fp(i,1,m) {
            char c=0;
            while (c!='Q'&&c!='C') c=getchar();
            if (c=='Q') {
                int l,r,k; scanf("%d%d%d",&l,&r,&k);
                a[++tot]=(rec) {l,r,k,2,++qes};
            } else {
                int pos,t; scanf("%d%d",&pos,&t);
                a[++tot]=(rec) {tmp[pos],-1,0,1,pos};//-1删除
                tmp[pos]=t;
                a[++tot]=(rec) {tmp[pos],1,0,1,pos}; //+1增加
            }
        }
        solve(1,tot,-inf,inf);
        fp(i,1,qes) printf("%d
    ",ans[i]);
        return 0;
    }
    带修改区间K大数 整体二分算法

    整体二分的复杂度的话,

    离散化优化整体二分 $O(m {log_2}^2 n)$

    不优化(也差不多) $O(m {log_2} n log_2 INF)$

  • 相关阅读:
    【转】浅谈 C++ 中的 new/delete 和 new[]/delete[]
    指针与const
    【面经】【转】C程序的内存布局
    【面经】二叉树层次遍历
    【面经】【转】C++类型转换
    【转】fastdb中的数据字典
    AtCoder AGC043D Merge Triplets (DP、组合计数)
    AtCoder AGC024F Simple Subsequence Problem (字符串、DP)
    Codeforces 1110G Tree-Tac-Toe (博弈论)
    Luogu P5244 [USACO2019Feb Platinum] Mowing Mischief (动态规划、决策单调性)
  • 原文地址:https://www.cnblogs.com/ljc20020730/p/10424336.html
Copyright © 2011-2022 走看看