校内模拟赛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;
}


感受

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

posted @ 2019-07-26 10:41  MisakaMKT  阅读(131)  评论(0编辑  收藏  举报