zoukankan      html  css  js  c++  java
  • 重逢可持久化线段树

    与可持久化线段树重逢在了Codeforces Educational Round 80的E题,还好及时意识到了是可持久化线段树的经典操作——统计区间内不同的数字个数。

    于是乎,赛后又重新复习了一下可持久化线段树,

    可持久化意味着需要建立多个版本,为了节省空间,利用增量修改的想法,我们只去记录那些发生变化的部分(因此要求整个数据结构的结构不能发生变化,针对那些带旋转的平衡树),

    往往要追求的目标是发生修改的部分是从根到叶子的一条链,树高是O(logN)的,因此只会新增O(logN)个节点,最后的节点总数是O(N+MlogN)。

    其实这就限制了绝大部分修改都只能是单点修改。

    而建立完可持久化线段树之后呢,无论是修改还是查询操作,我们在确定完在哪个版本之后,都只需要当成是在一棵线段树上考虑即可。

    我觉得思考一个问题是否可以用可持久化线段树来做,关键还是思考线段树维护的是什么信息,从而能否把修改操作变成单点修改,或者查询的操作可以如愿完成。

    然后有些查询操作需要满足可减性,比如最常见的区间第k大。

    然后有时候会把问题搬到树上,一般是再多考虑LCA或者LCA的父亲的情况,具体的应用会非常灵活,需要具体问题具体分析。

    静态区间第K大——我们建立权值线段树,每个节点代表的是权值的出现次数,非叶子节点记录的是区间内的权值的出现次数,然后从左到右往上一个版本插入新的数形成新的版本。查找第K大的时候从上到下直接根据数值关系找就行了。

    不强制在线的话还可以用离线+树状数组写,常数会小很多。(关于离线处理的数据结构题,过几天会总结一下之前遇到过的几道,待更)

    区间内不同的数字个数——这个我觉得非常巧妙,因为查询的是一个区间,当右端点固定的时候,肯定是越往右的数字越容易出现在区间中,因为我们把那些到目前为止最后一次出现该数字的位置设为1,其余都为0,然后在区间内查找[l,r]范围内的区间和即可。修改都是单点修改。

    区间内前K小的值的总和——其实只要在第K大的基础上再维护个权值区间对应的总和就可以了。

    此外,有些问题可能用二分的思想会降低思维难度,然后里面再套个可持久化线段树,但是这样复杂度有时候会多一个log,有时候可以直接在主席树上一路往下找,省掉那个log。

    最后,贴一下这次E题我的做法吧,观察到一个数字出现过就变成了第一,然后就不可能往前,要么不动,要么往后移,直到下次出现。那么相邻两次出现的间隔中出现的不同的数字个数就是一个极大值,去更新最大值即可,最小值比较朴素,判断是否出现过即可。为了避免边界条件,把原数组扩充了一下,一定程度导致空间有点吃紧。

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int maxn=600005;
    struct node{
        int l,r,sum;
    }T[maxn*32];
    int n,m,a[maxn],cnt,root[maxn],pos[maxn];
    vector<int> v[maxn+5];
    int mn[maxn+5],mx[maxn+5];
    void update(int &now,int pre,int val,int l,int r,int pos){
        T[++cnt]=T[pre],T[cnt].sum+=val,now=cnt;
        if(l==r) return;
        int mid=l+r>>1;
        if(pos<=mid) update(T[now].l,T[pre].l,val,l,mid,pos);
        else update(T[now].r,T[pre].r,val,mid+1,r,pos);
    }
    int query(int L,int R,int l,int r,int rt){
        if(L<=l&&R>=r) return T[rt].sum;
        int ans=0,mid=l+r>>1;
        if(L<=mid) ans+=query(L,R,l,mid,T[rt].l);
        if(R>mid) ans+=query(L,R,mid+1,r,T[rt].r);
        return ans;
    }
    int main(){
        scanf("%d",&n);
        scanf("%d",&m);
        for (int i=1;i<=n;i++) a[i]=n-i+1;
        for (int i=n+1;i<=n+m;i++) scanf("%d",&a[i]);
        for (int i=1;i<=n+m;i++) v[a[i]].push_back(i);
        for (int i=1;i<=n+m;i++){
            if(pos[a[i]]){
                update(root[i],root[i-1],-1,1,n+m,pos[a[i]]);
                update(root[i],root[i],1,1,n+m,i);
            }else update(root[i],root[i-1],1,1,n+m,i);
            pos[a[i]]=i;
        }
        for (int i=1;i<=n;i++)
        if (v[i][v[i].size()-1]>n) {
            mn[i]=1;
        }
        else mn[i]=i;
        for (int i=1;i<=n;i++) {
            for (int j=0;j<v[i].size()-1;j++) {
                int lef=v[i][j],rig=v[i][j+1];
                mx[i]=max(mx[i],query(lef,rig,1,n+m,root[rig]));
            }
            mx[i]=max(mx[i],query(v[i][v[i].size()-1],n+m,1,n+m,root[n+m]));
        }
        for (int i=1;i<=n;i++) printf("%d %d
    ",mn[i],mx[i]);
        return 0;
    }
  • 相关阅读:
    如何正确使用 Composer 安装 Laravel 扩展包
    sql之left join、right join、inner join的区别
    mysql decimal类型与decimal长度用法详解
    mysql数据库操作
    linux下如何查看某软件是否已安装
    MySQL数据类型和常用字段属性总结
    mysql数据库字段类型的选择原则
    linux shell 指令 诸如-d, -f, -e之类的判断表达式
    常用的Homebrew命令
    windows下安装php5.5的redis扩展
  • 原文地址:https://www.cnblogs.com/xyw5vplus1/p/12203484.html
Copyright © 2011-2022 走看看