zoukankan      html  css  js  c++  java
  • 整体二分——离线整体处理

    整体二分:

    对于一般二分,我们会logn外层二分一个答案,然后内层O(n)或者O(nlogn)检验。

    然鹅一些有二分性质的题,询问比较多,每次逐一二分会T飞。

    但是二分的范围相对固定,二分的对象也固定,而且还可以离线的话,就可以用整体二分实现。

    基本思路是,二分一个mid,考虑<=mid哪些询问能够满足,根据满足与否,把询问左右下放,然后分治处理。

    这是一般的二分的升级版。可以把所有的修改询问放在一起,用一个整体的二分来实现。一次性处理所有的修改,询问。

    支持区间询问,区间修改,但是必须离线操作。

    细节,边界情况考虑要到位,代码不长,但是初学的时候不是很好写。

    关键是把二分理解好。

    首先考虑不修改:

    二分答案是二分答案的范围然后不断缩小,整体二分也一样。

    div(A,l,r)表示,A集合所代表的询问的答案在l~r里。

    每次找到一个mid,计算每个询问在mid+1~r或者l~mid中是否可以。

    然后把这个询问放进下一层可以的位置的集合里面递归下去。

    当l==r时,A集合中的询问的答案就都是l了。

    复杂度和分治类似。logC层,每层一般O(n)或者O(nlogn)

    例题:

    区间第k小。(主席树经典例题)

    我们用整体二分做。

    把所有的数从小到大排一个序,放进另一个数组b里。

    在每个div(A,l,r)中,

    找到mid=l+r>>1,从排好序的数组b中,二分找到数值为l的位置,数值为mid的位置,

    把大小为l~mid的数加入一个位置为下标树状数组里,(有就加1)然后循环当前层的A询问集合,对于qi询问

    用树状数组找[li,ri]的大小sum,也就是[li,ri]中,数值在[l,mid]的数有多少个。

    如果大于等于ki,说明qi的答案一定在[l,mid]中,放进左儿子集合。

    否则,qi的答案一定在[mid+1,r],令ki-=sum,放进右儿子集合。

    递归之前,把树状数组上改变的位置再-1

    分别递归左右儿子。

    l==r的时候统计答案。

    但是,复杂度O(nlogn^2)比主席树慢,而且还必须离线(辣鸡)。

    51nod 1175:(不过这个题是区间第k大)

    (把mid+1~r放进去即可)

    #include<bits/stdc++.h>
    using namespace std;
    const int N=50000+10;
    const int inf=1e9+2;
    int f[N];
    
    struct node{
        int l,r;
        int ans;
        int kth;
    }q[N];
    
    int n,m;
    void add(int x,int c){for(;x<=n;x+=x&(-x))f[x]+=c;}
    int query(int x){int ret=0;for(;x;x-=x&(-x))ret+=f[x];return ret;}
    int delpool,cur;
    int dp[100];
    vector<int>s[100];
    
    
    int a[N];
    int    nc(){
        int r=delpool?dp[delpool--]:++cur;
        s[r].clear();return r;
    }
    void dele(int x){
        dp[++delpool]=x;
    }
    struct po{
        int val,id;
        bool friend operator <(po a,po b){
            if(a.val==b.val) return a.id<b.id;
            return a.val<b.val;
        }
    }p[N],kk;
    void slo(int id,int l,int r){
     
        if(l==r){
            for(int i=0;i<s[id].size();i++){
                q[s[id][i]].ans=l;
            }
            return;
        }
        int mid=(l+r)/2;
        int lst=-1,rnd=-1;
        int L=1,R=n;
        while(L<=R){
            int M=(L+R)>>1;
            if(p[M].val>=mid+1) lst=M,R=M-1;
            else L=M+1;
        }
        L=1,R=n;
        while(L<=R){
            int M=(L+R)>>1;
            if(p[M].val<=r) rnd=M,L=M+1;
            else R=M-1;
        }
        
        if(lst!=-1&&rnd!=-1){
            for(int i=lst;i<=rnd;i++){
                add(p[i].id,1);
            }
        }
        
        int le=nc(),ri=nc();
        bool fle=false,fri=false;
        for(int i=0;i<s[id].size();i++){
            int o=s[id][i];
            
            int sum=query(q[o].r)-query(q[o].l-1);
            
            if(sum>=q[o].kth){
                fri=true;
                s[ri].push_back(o);
            }
            else{
                fle=true;
                q[o].kth-=sum;
                s[le].push_back(o);
            }
        }
        if(lst!=-1&&rnd!=-1){
            for(int i=lst;i<=rnd;i++){
                add(p[i].id,-1);
            }
        }
        
        if(fle)slo(le,l,mid);
        dele(le);
        if(fri)slo(ri,mid+1,r);
        dele(ri);
    }
    int mi=inf,mx=0;
    int main()
    {
        scanf("%d",&n);
        for(int i=1;i<=n;i++){
            scanf("%d",&a[i]);
            mi=min(mi,a[i]);
            mx=max(mx,a[i]);
            p[i].val=a[i];
            p[i].id=i;
        }
        sort(p+1,p+n+1);
    
        scanf("%d",&m);
        
        int st=nc();
        for(int i=1;i<=m;i++){
            scanf("%d%d%d",&q[i].l,&q[i].r,&q[i].kth);
            q[i].l++;q[i].r++;
            s[st].push_back(i);
        }
    
        slo(st,mi,mx);
        
        for(int i=1;i<=m;i++){
            printf("%d
    ",q[i].ans);
        }
        return 0;
    }

     注意:

    1.第k大,把mid+1~r放进去。

    2.直接l~mid,mid+1~r递归,是一定能找到答案的。

    3.注意vector垃圾回收(下面讲解)

    4.当sum<kth的时候,q[o].kth-=sum别忘了

    至于A集合怎么存储?

    1.vector存储。但是每层另开两个vector一定MLE。发现,递归类似于dfs,一定先深度优先遍历,再回溯。

    回溯之后,之前走过的地方的vector已经没有用了,但是还存在,占着空间。

    其实,当基础的答案区间是-1e18~1e18的时候,最多深度是二分65次,每次最多产生两个儿子,但是一直沿一个儿子走下去,

    所以,最坏情况下,最大的有用vector数量不过130个。

    开一个全局的vector<int>p[130]其实足够了。

    所以必须考虑一下垃圾回收。

    vector<int>p[130];
    int delepool[130];
    int dp;
    int tc;
    void dele(int s){
        delepool[++dp]=s;
    }
    int nc(){
        int r=dp?delepool[dp--]:++tc;
        p[r].clear();
        return r;
    }

    这样,div(A,l,r),A就是当前存储的vector的编号了。

    这样,每次找一下left儿子编号le=nc(),右儿子ri=nc()

    然后,递归完回溯,dele(le),dele(ri)即可。

    亲测可以实现,完全正确。

    (upda:2018.12.18 :但是常数和空间会比较大,而且易错,不推荐,这是博主以前自己yy的做法)

    (注意:l==r判断回溯的时候就不要dele(id)了,因为,回溯之后,id作为左二子或者右儿子会再一次被dele。就错了。)

    2.引入两个临时的tmp[]结构体变量,tmp1,tmp2分别表示左儿子,右儿子询问集合。

    判断然后加入进去,注意,是把整个qi结构体复制进去。加入完了之后,设cnt1表示tmp1大小,cnt2表示tmp2大小,

    把tmp1中的qi依次加入A中,tmp2中的qi再在后面依次加入A中。

    总之,div(ql,qr,l,r)集合变成了一段连续的区间,

    分配完了之后,左二子变成了一段区间ql~ql+cnt1-1 右儿子就是ql+cnt1~qr

    这样,用区间的两个端点就可以代表整个询问集合了。

    //By YJC i207M
    void sol(int ql, int qr, int l, int r) {
        //..........
        int p = ql;
        for (ri i = 1; i <= cnt1; ++i) {
            q[p] = tmp1[i];
            ++p;
        }
        for (ri i = 1; i <= cnt2; ++i) {
            q[p] = tmp2[i];
            ++p;
        }
        sol(ql, ql + cnt1 - 1, l, mid); sol(ql + cnt1, qr, mid + 1, r);
    }

    注意,我们询问本身必须用结构体q[]存储,因为它们的位置可能随时变化,必须一起动。

    [国家集训队]矩阵乘法

    二分mid,二维树状数组处理0/1矩形和。然后递归即可。

    同样k-=now,

     Stamp Rally

    有修改操作怎么办?

    可以发现,对于一个询问,并不是所有的修改操作都对询问答案产生影响。

    所以,开始,我们把A集合中,按原来顺序加入所有的修改操作和询问操作,

    假设以mid+1~r为标准计算询问的归类:

    二分mid,循环A集合。

    1.如果是修改操作,对于修改的值小于等于mid的修改,放进左集合,表示会对答案在mid左边的询问产生影响。

    对于修改的值大于mid的修改,在一个数据结构上修改,然后放进右集合,表示会对答案在mid右边的询问产生影响。

    2.如果是查询操作,直接通过数据结构查询,在它前面的修改操作能影响的已经都加入数据结构里了,所以可以确定qi询问所属儿子集合。

    (有点类似cdq分治)

    不懂的话,看例题:

    [ZJOI2013]K大数查询

    题目描述

    有N个位置,M个操作。操作有两种,每次操作如果是1 a b c的形式表示在第a个位置到第b个位置,每个位置加入一个数c如果是2 a b c形式,表示询问从第a个位置到第b个位置,第C大的数是多少。

    N,M<=50000

    a<=b<=N

    1操作中abs(c)<=N

    2操作中c<=long long

    (保证输入合法)

    Solution:

    没错,这个可以用树套树解决。

    但是,整体二分更好写。

    相当于每个位置有一个桶,往里面不断加入数据。

    按照上面所说的,把所有的询问、修改按顺序加入A里。

    二分mid,

    修改操作,对于加入的c<=mid,放进左集合不管。

    否则,用一个线段树,以位置为下标,区间修改加1,加入c们。放进右集合。

    对于一个询问,

    线段树找[l,r]的sum,如果sum>=k,说明qi的答案一定在[mid+1,r]里面,放进右集合。

    否则,k-=sum,放进左集合。

    代码:(不知道为什么要的define int long long,害得调到凌晨)

    Code:(我用的vector存储,因为值域在-n到n之间,所以开35即可。)

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    typedef long long ll;
    const int N=50000+10;
    int n,m;
    struct node{
        int l,r;ll c;int id;int op;ll ans;
    }q[N];
    struct tr{
        int sum,ad;
    }t[N*4];
    int dp;
    int tc;
    vector<int>p[35];
    int delepool[35];
    void dele(int s){
        delepool[++dp]=s;
    }
    int nc(){
        int r=dp?delepool[dp--]:++tc;
        p[r].clear();
        return r;
    }
    int ans[N];
    int tot;
    void pushdown(int x,int l,int r){
        int mid=l+r>>1;
        t[x<<1].sum+=t[x].ad*(mid-l+1);
        t[x<<1|1].sum+=t[x].ad*(r-mid);
        t[x<<1].ad+=t[x].ad;
        t[x<<1|1].ad+=t[x].ad;
        t[x].ad=0;
    }
    void add(int x,int l,int r,int L,int R,int c){
        if(L<=l&&r<=R){
            t[x].sum+=c*(r-l+1);
            t[x].ad+=c;
            return;
        }
        pushdown(x,l,r);
        int mid=l+r>>1;
        if(L<=mid) add(x<<1,l,mid,L,R,c);
        if(mid<R) add(x<<1|1,mid+1,r,L,R,c);
        t[x].sum=t[x<<1].sum+t[x<<1|1].sum;
    }
    int query(int x,int l,int r,int L,int R){
        if(L<=l&&r<=R){
            return t[x].sum;
        }
        pushdown(x,l,r);
        int mid=l+r>>1;
        int ret=0;
        if(L<=mid) ret+=query(x<<1,l,mid,L,R);
        if(mid<R) ret+=query(x<<1|1,mid+1,r,L,R);
        return ret;
    }    
    void div(int id,ll l,ll r){
        if(l==r){
            for(int i=0;i<p[id].size();i++){
                if(q[p[id][i]].op==2) q[p[id][i]].ans=l;
            }
            return;
        }
        ll mid=(ll)floor(((double)1.0*l+(double)1.0*r)/((double)2));
        int le=nc(),ri=nc();
        bool fl=false,fr=false;
        for(int i=0;i<p[id].size();i++){
            if(q[p[id][i]].op==1){
                if(q[p[id][i]].c<=mid) p[le].push_back(p[id][i]);
                else {
                    p[ri].push_back(p[id][i]);
                    add(1,1,n,q[p[id][i]].l,q[p[id][i]].r,1);
                }
            }
            else{
                int sum=query(1,1,n,q[p[id][i]].l,q[p[id][i]].r);
                if(sum>=q[p[id][i]].c){
                    fr=true;
                    p[ri].push_back(p[id][i]);
                }
                else
                {
                    fl=true;
                    q[p[id][i]].c-=sum;
                    p[le].push_back(p[id][i]);
                }
            }
        }
        for(int i=0;i<p[id].size();i++){
            if(q[p[id][i]].op==1){
                if(q[p[id][i]].c>mid) {
                    add(1,1,n,q[p[id][i]].l,q[p[id][i]].r,-1);
                }
            }
        }
        if(p[le].size()&&fl)div(le,l,mid);
        dele(le);
        if(p[ri].size()&&fr)div(ri,mid+1,r);
        dele(ri);
    }
    signed main()
    {
        scanf("%d%d",&n,&m);
        int st=nc();
        for(int i=1;i<=m;i++){
            scanf("%d%d%d%lld",&q[i].op,&q[i].l,&q[i].r,&q[i].c);
            q[i].id=i;
            p[st].push_back(i);
        }
        div(st,-n,n);
        for(int i=1;i<=m;i++){
            if(q[i].op==2){
                printf("%lld
    ",q[i].ans);
            }
        }
        return 0;
    }
  • 相关阅读:
    oracle如何查询哪个表数据量大
    SecureRandom生成随机数超慢 导致tomcat启动时间过长的解决办法
    smartctl----硬盘状态监控
    Oracle数据库的状态查询
    jdbc连接数据库使用sid和service_name的区别
    V$INSTANCE 字段说明
    V$PROCESS和V$SESSION,以及使用这两个视图能做什么
    NetOps Defined
    POI 海量数据
    HTML5 CSS3 诱人的实例: 3D立方体旋转动画
  • 原文地址:https://www.cnblogs.com/Miracevin/p/9499744.html
Copyright © 2011-2022 走看看