zoukankan      html  css  js  c++  java
  • 校内模拟赛T5:连续的“包含”子串长度( nekameleoni?) —— 线段树单点修改,区间查询 + 尺取法合并

    nekameleoni

    区间查询和修改
    给定N,K,M(N个整数序列,范围1~K,M次查询或修改)
    如果是修改,则输入三个数,第一个数为1代表修改,第二个数为将N个数中第i个数做修改,第三个数为修改成这个数(例如1 3 5就是修改数组中第3个数,使之变为5)
    如果是查询,则输入一个数2,查询N个数中包含1~K每一个数的最短连续子序列的长度
    输入
    第一行包含整数N、K和M(1 ≤ N,M ≤ 100000,1 ≤ K ≤ 50)
    第二行输入N个数,用空格隔开,组成该数组
    然后M行表示查询或修改
    若是1 p v(1 ≤ p ≤ N,1 ≤ v ≤ K)
    若是2则是询问1~K的最短连续子序列长度。
    输出
    输出必须包含查询的答案,如果不存在则输出-1。

    分数分布
    对于30%的数据:1≤M,N≤5000。

    样例输入1
    4 3 5
    2 3 1 2
    2
    1 3 3
    2
    1 1 1
    2

    样例输出1
    3
    -1
    4

    样例输入2
    6 3 6
    1 2 3 2 1 1
    2
    1 2 1
    2
    1 4 1
    1 6 2
    2

    样例输出2
    3
    3
    4

    解释一下题意,如果询问为2则输出包含1-k的最长连续序列,否则进行单点修改。

    单点修改,区间查询,明显可以使用线段树,但如何进行区间的维护却是此题的难点。


    考虑使用3个量来维护线段树的每一个节点:

    1.前缀中,用(x_{i})表示含(x_{i})种不同数字的连续的序列。存储每个(x_{i})的状态(也就是哪(x_{i})个不同的数字,这里可以使用二进制压50位来实现)。

    对于多个前缀,如果它们包含的不同的数字相同,我们只需要存储长度最小的那一个,这样可以保证结果最优。

    同理后缀也可以这么实现。

    2.ans,表示在当前节点表示的区间中,包含(k)个不同的数的最短区间的长度,也就是最终要输出的答案。这样的好处是我们可以直接输出根节点的(ans)作为答案。


    有了前缀和后缀,我们就可以进行合并了。对于节点(t)的左儿子(l)和右儿子(r),当我们要将(l)(r)合并成(t)时,只需要将(l)的后缀和(r)的前缀进行合并,得到满足条件的最小值,将它与(l)的ans,和(r)的ans进行比较,最优的值便可以最为(t)的ans。


    前缀和后缀的维护比较复杂,这些细节的问题我会在代码中的注释解释(比如二进制的使用,还有前缀和后缀的更新操作都很复杂。)

    这里探讨的主要是前缀和后缀的合并操作。

    (l)的后缀有(k_{1})个,(r)的后缀有(k_{2})个,我们可以考虑暴力枚举每一个(k_{1})(k_{2})来合并,但这样做时间复杂度过高,所以便可以使用尺取法。

    我们可以发现(l)若的第(i)个后缀与(r)的第(j)个后缀合并后满足有(k)个不同的数字,那么便无需考虑(j)之后的后缀。

    这便是尺取法的精髓思想。如果你还不懂尺取法,可以去网上找博客看看(或许我以后会写相关的博客?)。


    尺取法的时间复杂度是线性的,所以我们整个程序的时间复杂度是(O(n_{logn}k))


    代码

    #include <iostream>
    #include <cstdio>
    using namespace std;
    
    #define N 100010
    #define MAXN 262144
    #define T 131071
    #define LL long long
    #define inf 0x7f7f7f7f
    typedef pair<LL,int> P;
    
    struct Fake {
        P suf[52],pre[52];
        int ans,sum;
        Fake():ans(inf) {};
    }Fuck[MAXN];
    
    int n,k,m;
    
    void Merge(int t,int l, int r) {
        int ans=inf;
        Fuck[t].ans=min(Fuck[l].ans,Fuck[r].ans);//维护ans
        Fuck[t].sum=0;
        int lenpre=0,lensuf=0;
        for(int i=0;i<Fuck[l].sum;i++)
            Fuck[t].pre[lenpre++]=Fuck[l].pre[i];//将左儿子的前缀继承到当前节点的前缀
        for(int i=0;i<Fuck[r].sum;i++) {
            if(lenpre==0 ||( Fuck[r].pre[i].first & Fuck[t].pre[lenpre-1].first) != Fuck[r].pre[i].first) {//如果右节点的前缀包含的不同数数量比左儿子的前缀数量还多,则可以
                Fuck[t].pre[lenpre]=Fuck[r].pre[i];//添加进来
                if(lenpre>0) Fuck[t].pre[lenpre].first|=Fuck[t].pre[lenpre-1].first;
                lenpre++;
            }
        }
        for(int i=0;i<Fuck[r].sum;i++)//后缀处理同理
            Fuck[t].suf[lensuf++]=Fuck[r].suf[i];
        for(int i=0;i<Fuck[l].sum;i++) {
            if(lensuf==0 ||( Fuck[l].suf[i].first & Fuck[t].suf[lensuf-1].first) != Fuck[l].suf[i].first) {
                Fuck[t].suf[lensuf]=Fuck[l].suf[i];
                if(lensuf>0) Fuck[t].suf[lensuf].first|=Fuck[t].suf[lensuf-1].first;
                lensuf++;
            }
        }
        Fuck[t].sum=lenpre;
        int j=0;
        for(int i=Fuck[l].sum-1;i>=0;i--) {//尺取法合并操作
            while(j<Fuck[r].sum) {
                if((Fuck[r].pre[j].first | Fuck[l].suf[i].first) == ((1ll<<k)-1))
                    break;
                j++;
            }
            if(j<Fuck[r].sum) Fuck[t].ans=min(Fuck[t].ans,Fuck[r].pre[j].second - Fuck[l].suf[i].second+1);
        }
    }
    
    void update(int pos,int x) {
        int now=T+pos;
        Fuck[now].suf[0]= make_pair(1ll<<(x-1),pos) ;//用一个pair类型来表示一个前缀
        Fuck[now].pre[0]= make_pair(1ll<<(x-1),pos) ;//first为一个二进制数,表示包含了哪些
        Fuck[now].ans=inf;//不同的数字,pos为对应的下标,用于计算答案
        Fuck[now].sum=1;//sum统计相应的前后缀数量
        while(now/2) {//循环型线段树,从儿子往根节点操作
            now/=2;
            Merge(now,now<<1,now<<1|1);
        }
    }
    
    int main() {
        cin>>n>>k>>m;
        for(int i=1,A;i<=n;i++) {
            cin>>A;
            update(i,A);
        }
        for(int i=1,t,pos,x;i<=m;i++) {
            cin>>t;
            if(t==2) {
                if(Fuck[1].ans==inf) cout<<"-1"<<endl;
                else cout<<Fuck[1].ans<<endl;
            }
            else {
                cin>>pos>>x;
                update(pos,x);
            }
        }
        return 0;
    }
    
    

    感受

    做了这道题,算是让我接受了线段树和二进制压位运算的洗礼吧。线段树真的是十分灵活的数据结果呀。

  • 相关阅读:
    scala程序启动时,Could not locate executable nullinwinutils.exe in the Hadoop binaries解决方案
    binlog_format的模式有哪几种?各自的特点是?
    解决IDEA控制台junit不能用Scanner输入问题
    【转】什么是乐观锁,什么是悲观锁
    lyt经典版MySQL基础——流程控制结构
    ambari 2.5.0源码编译安装
    linux(centeros)svn的安装
    机器学习
    算法思想整理
    lucene
  • 原文地址:https://www.cnblogs.com/MisakaMKT/p/11248819.html
Copyright © 2011-2022 走看看