2019暑假七考——连续的“包含”子串长度[nekameleoni]——(线段树高端操作,妙用尺取)

7 篇文章 0 订阅
5 篇文章 0 订阅


题目(3000ms)

描述
区间查询和修改

给定 N , K , M N,K,M N,K,M N N N个整数序列,范围 1   K 1~K 1 K M M M次查询或修改)

如果是修改,则输入三个数,第一个数为1代表修改,第二个数为将N个数中第i个数做修改,第三个数为修改成这个数(例如1 3 5就是修改数组中第3个数,使之变为5)

如果是查询,则输入一个数2,查询N个数中包含1~K每一个数的最短连续子序列的长度
输入
第一行包含整数 N 、 K N、K NK M M M

第二行输入N个数,用空格隔开,组成该数组

然后M行表示查询或修改

• “1 p v” - change the value of the pth number into v

• “2” - what is the length of the shortest contiguous subarray of the array containing all the integers from 1 to K
输出
输出必须包含查询的答案,如果不存在则输出-1
范围
1 ≤ N , M ≤ 100000 , 1 ≤ K ≤ 50 , 1 ≤ p ≤ N , 1 ≤ v ≤ K 1 ≤ N,M ≤ 100000,1 ≤ K ≤ 50,1 ≤ p ≤ N,1 ≤ v ≤ K 1N,M100000,1K501pN,1vK
样例

样例输入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

思路

既然都看见这种修改和查询的操作了,第一反应肯定就是线段树或者莫队,显而易见,此题不用莫队,因为中间有修改操作(也有可能是我太弱,不知道某些高端操作)

线段树中我们就维护包含K个数的序列的最小长度,当然,这东西一看就知道不好维护,我们需要借助另一些东西。想想,这个最小长度肯定是一个区间,那么如何从左右儿子当中维护出这样一个区间呢,那便是左儿子的所有后缀序列和右儿子的所有前缀序列,这样一来,当前节点的所有区间就都可以表示出来了。若当前节点是左儿子,那我们就需要它的后缀,而它的后缀其实最开始是来源于它的右儿子,所以对于每一个区间,我们都需要存储它的前缀序列和后缀序列

如果用数组,肯定爆炸,看一看数据范围 k ≤ 50 k \leq 50 k50,那肯定压缩状态呀,这个数二进制位上第 i i i位为 1 1 1即表示数 i i i在此序列中出现过,
对于全部序列,肯定有些序列的状态是相同的(如112,12),我们肯定要取长度最小的,那么如何比较状态相同呢?用 & \& &符号,因为 & \& &是先把两个数转换为二进制,进行比较,两位同时为1那么值才为1,其他情况均为0,所以当前状态与原数组中的状态 & \& &一下,若等于当前状态,说明两个序列在本质上是相同的,不用更新,而因为 k ≤ 50 k \leq 50 k50,所以序列最多的个数也不会超过50,而考虑到右儿子的前缀是从中间开始的,所以我们要将右儿子的前缀与前面的连起来(状态继承),两个序列 | 一下就行了

当前区间的ans肯定要先在左右儿子的ans中取最小值,而因为左右区间结合了,所以我们还要检查一下中间的序列会不会有更好的答案,这时候,就要用到尺取法了,在尺取的时候,你会发现有个问题怎么把这个序列的长度搞出来,所以,我们在存储序列的状态时,顺手就把序列结束的位置存进去,尺取的时候就可以更新答案了

由于递归线段树更新会耗费大量时间,所以我们选择从叶节点自底向上更新,就减少了递归的时间,那么如何找到此节点在线段树中的位置呢?我们就把从这个线段树看做一个完全二叉树,那么它的叶节点就有 n n n个,那么它的层数就有 l o g n log n logn层,而除去最后一层,上面总共的节点数就有 2 l o g n − 1 2^{logn}-1 2logn1个,而叶节点的位置也就是 i + 2 l o g n − 1 i+2^{logn}-1 i+2logn1


Code
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define INF 0x3f3f3f3f
#define LL long long
#define IL inline
#define Pr pair<long long, int>
#define M 100005
int offet;

struct node{
    Pr pre[55], sub[55];//一个前缀,一个后缀,pair中一个状态,一个序列的结束位置
    int v_sum;//序列的个数
    int ans;

    node(){ ans = INF; }

}T[M*2+70000];

int n, k, m, q, u, v;
LL goal;

IL int qkp(int x, int y){//计算上面节点的总数
    int sum = 1;
    while( y ){
        if( y&1 )
            sum = sum*x;
        x *= x;
        y >>= 1;
    }
    return sum;
}

IL void calc(int x, int l, int r){
    T[x].ans = INF;
    T[x].v_sum = 0;
    int lenpre = 0, lensub = 0;
    for(int i = 0; i < T[l].v_sum; ++ i)//此区间的前缀是先从左儿子的前缀开始的
        T[x].pre[lenpre++] = T[l].pre[i];
    for(int i = 0; i < T[r].v_sum; ++ i){//右儿子的前缀需要练起来
        if( !lenpre || (T[r].pre[i].first&T[x].pre[lenpre-1].first) != T[r].pre[i].first ){//若状态不同
            T[x].pre[lenpre] = T[r].pre[i];
            if( lenpre > 0 ){
                T[x].pre[lenpre].first |= T[x].pre[lenpre-1].first;//状态继承
            }
            lenpre++;
        }
    }
    for(int i = 0; i < T[r].v_sum; ++ i)//此区间的后缀是先从右儿子的后缀开始的
    	T[x].sub[lensub++] = T[r].sub[i];
    for(int i = 0; i < T[l].v_sum; ++ i){
        if( !lensub || (T[l].sub[i].first&T[x].sub[lensub-1].first) !=  T[l].sub[i].first ){//状态不同
            T[x].sub[lensub] = T[l].sub[i];
            if( lensub > 0 ){
                T[x].sub[lensub].first |= T[x].sub[lensub-1].first;//状态继承
            }
            lensub++;
        }
    }
    T[x].ans = min(T[l].ans, T[r].ans);
    T[x].v_sum = lenpre;
    int cl = T[l].v_sum-1, cr = 0;
    LL SS = 0;
    while( cl >= 0 && cr < T[r].v_sum ){//尺取(有毒)
        while( cr < T[r].v_sum  ){
            SS = T[l].sub[cl].first|T[r].pre[cr].first;
            if( SS >= goal )
                break;
            cr++;
        }
        while( cr < T[r].v_sum && SS >= goal && cl >= 0 ){
            T[x].ans = min(T[x].ans, T[r].pre[cr].second-T[l].sub[cl].second+1);
            cl--;
            SS = T[l].sub[cl].first|T[r].pre[cr].first;
        }
    }
}

IL void update(int i, int val){
    i += offet;//计算出在线段树中的位置
    T[i].pre[0] = Pr(1ll<<val, i-offet);//更新本节点状态
    T[i].sub[0] = Pr(1ll<<val, i-offet);
    T[i].ans = INF;
    T[i].v_sum = 1;
    while( i>>1 ){//自底向上更新
        i >>= 1;
        calc(i, i<<1, i<<1|1);
    }
}

int main(){
	scanf("%d%d%d", &n, &k, &m);
	goal = (1ll<<k)-1;
	offet = qkp(2, ceil(log(double(n))/log(2.0)))-1;
	for(int i = 1; i <= n; ++ i){
        scanf("%d", &u);
        update(i, u-1);
	}
	for(int i = 1; i <= m; ++ i){
        scanf("%d", &q);
        if( q == 2 ){
            if( T[1].ans != INF )
                printf("%d\n", T[1].ans);
            else printf("-1\n");
        }
        else if( q==1 ) {
            scanf("%d%d", &u, &v);
            update(u, v-1);
        }
	}
}


代码实现Tips
  • 线段树自底向上更新,看做完全二叉树,计算除叶节点的节点个数
  • 整数代表序列状态,| 继承状态,&检查状态
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值