【洛谷】P5826 【模板】子序列自动机

题目地址:

https://www.luogu.com.cn/problem/P5826

题目描述:
给定一个长度为 n n n的正整数序列 a a a,有 q q q次询问,第 i i i次询问给定一个长度为 L i L_i Li的序列 b i b_i bi,请你判断 b i b_i bi是不是 a a a的子序列。序列 a a a和所有 b i b_i bi中的元素都不大于一个给定的正整数 m m m。本题中,若 x x x y y y的子序列,则等价于存在一个单调递增序列 z z z,满足 ∣ z ∣ = ∣ x ∣ ∣z∣=∣x∣ z=x z ∣ x ∣ ≤ ∣ y ∣ z_{∣x∣}≤∣y∣ zxy,且 ∀ i ∈ [ 1 , ∣ x ∣ ] , y z i = x i ∀i∈[1, ∣x∣], y_{z_i}=x_i i[1,x],yzi=xi。其中 ∣ x ∣ , ∣ y ∣ , ∣ z ∣ ∣x∣, ∣y∣, ∣z∣ x,y,z分别代表序列 x , y , z x, y, z x,y,z的长度, x i , y i , z i x_i, y_i, z_i xi,yi,zi分别代表序列 x , y , z x,y,z x,y,z的第 i i i项。

输入格式:
每个测试点有且仅有一组数据。输入的第一行是四个用空格隔开的整数,分别代表type, n n n q q q m m m。其中type代表测试点所在的子任务编号,其余变量的含义见题目描述。输入的第二行是 n n n个用空格隔开的整数,第 i i i个数字代表序列 a a a的第 i i i个元素 a i a_i ai。第 3 3 3行至第 ( q + 2 ) (q + 2) (q+2)行,每行代表一次询问。第 ( i + 2 ) (i + 2) (i+2)行的输入格式为:第 ( i + 2 ) (i + 2) (i+2)行的行首有一个整数 l i l_i li,代表第 i i i次询问的序列长度。一个空格后有 l i l_i li个用空格隔开的整数。该行的第 ( j + 1 ) (j + 1) (j+1)个整数代表序列 b i b_i bi的第 j j j个元素 b i , j b_{i, j} bi,j

输出格式:
对于每次询问,输出一行一个字符串,若给定的序列是 a a a的子序列,则输出Yes,否则输出No。

序列自动机是一种有限状态自动机,给定一个字符串 s s s,该自动机根据 s s s构建,可以用来判断另一个字符串 t t t是否是 s s s的子序列,并且保证在 O ( min ⁡ { l s , l t } ) O(\min\{l_s,l_t\}) O(min{ls,lt})时间内完成(当然一般情况下 t t t的长度远小于 s s s的长度,所以可以认为是 O ( l t ) O(l_t) O(lt))。该自动机可以由一个二维数组 A A A表示, A [ k ] [ c ] A[k][c] A[k][c]表示当匹配到 s s s的第 k k k个字符处的时候(这里下标从 1 1 1开始,是为了方便用数组实现),接下来如果读入了字符 c c c,应该跳转到哪一个位置。初始位置在 0 0 0,接下来每读入一个字符,就会跳转到该字符在当前位置之后第一次出现的位置;如果该字符之后不存在,则规定跳转到了 0 0 0(所以位置 0 0 0可以标记不是子序列的情况)。例如对于字符串"abcb",我们要判断"abb"是否是其子序列,初始位置为 0 0 0,先读入 a a a,那么 A [ 0 ] [ a ] = 1 A[0][a]=1 A[0][a]=1,因为 a a a第一次出现在下标 1 1 1的位置,就跳到了 1 1 1,接下来 A [ 1 ] [ b ] = 2 A[1][b]=2 A[1][b]=2,因为之后第一次出现 b b b是在下标 2 2 2的位置,就跳到了 2 2 2,再接下来 A [ 2 ] [ b ] = 4 A[2][b]=4 A[2][b]=4,因为在 2 2 2之后第一次出现 b b b的位置是 4 4 4,再接下来"abb"遍历完了,说明其是个子序列。再比如判断"aba"是否是其子序列,则跳转过程是 0 → A [ 0 ] [ a ] = 1 → A [ 1 ] [ b ] = 2 → A [ 2 ] [ a ] = 0 0\to A[0][a]=1\to A[1][b]=2\to A[2][a]=0 0A[0][a]=1A[1][b]=2A[2][a]=0,即遍历到最后一个 a a a的时候跳转到了 0 0 0,则说明不是子序列。子序列自动机的建立过程可以从后向前用递推来实现,初始条件 A [ l s ] [ α ] = 0 A[l_s][\alpha]=0 A[ls][α]=0,代表当前已经在 s s s的最后一个字符的位置了,后面再也没有字符了;接着向前递推: A [ k ] [ α ] = { A [ k + 1 ] [ α ] , α ≠ s [ k + 1 ] k + 1 , α = s [ k + 1 ] A[k][\alpha]=\begin{cases} A[k+1][\alpha], \alpha \ne s[k+1]\\k+1,\alpha=s[k+1] \end{cases} A[k][α]={A[k+1][α],α=s[k+1]k+1,α=s[k+1]这里的 s s s的下标是从 1 1 1开始的。这个递推关系式很好理解,如果某个位置的后一个位置就是当前输入字符,那显然接下来就要跳转到后一个位置;否则按照定义,跳转到从后一个位置之后的第一次出现的位置。但是本题如果直接开数组或者unordered_map来做爆空间。可以用vector动态开空间,然后用二分来解决。每个vector存某个数出现的所有位置,跳转的时候找大于当前位置的最前的匹配的位置。代码如下:

#include <iostream>
#include <vector>
using namespace std;

const int N = 1e5 + 10;
int n, q, m;
int a[N];
vector<int> pos[N];

// 找在i之后第一次出现x的位置,若不存在则返回0
int get_larger(int x, int i) {
    if (!pos[x].size()) return 0;

    int l = 0, r = pos[x].size() - 1;
    while (l < r) {
        int mid = l + (r - l >> 1);
        if (pos[x][mid] > i) r = mid;
        else l = mid + 1;
    }

    return pos[x][l] > i ? pos[x][l] : 0;
}

int main() {
    int type;
    scanf("%d%d%d%d", &type, &n, &q, &m);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        pos[a[i]].push_back(i);
    }

    while (q--) {
        int len;
        scanf("%d", &len);
        bool success = true;
        int cur = 0;
        while (len--) {
            int x;
            scanf("%d", &x);
            cur = get_larger(x, cur);
            if (!cur || !success) success = false;
        }

        if (success) printf("Yes\n");
        else printf("No\n");
    }

    return 0;
}

预处理时间复杂度 O ( n ) O(n) O(n),每次询问时间复杂度 O ( m log ⁡ n ) O(m\log n) O(mlogn) m m m是询问的序列长度,空间 O ( n ) O(n) O(n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值