Chgtaxihe's blog Chgtaxihe's blog
首页
练习
算法学习
读书笔记
小玩意

Chgtaxihe

首页
练习
算法学习
读书笔记
小玩意
  • 数论

  • 动态规划

  • 暴力

  • 图论

  • 数据结构

  • 字符串

    • AC自动机学习笔记
    • KMP/扩展KMP学习笔记
    • Lyndon分解学习笔记
    • Manacher学习笔记
    • 后缀数组
    • 后缀自动机学习笔记
    • 回文树
    • 子序列自动机
      • 做法1 - 二分查找
      • 做法2 - 可持久化线段树 (没必要)
      • 做法3 - 思维 - 桶(离线) *
    • 字符串基础
  • 几何

  • 博弈论

  • 其他

子序列自动机

# 前言

这是我第一次遇见如此简单易懂的"自动机"。

# 问题引入

给定一个长度为nnn的正整数序列AAA,有qqq次询问,每次询问给定一个长度为lil_ili​序列BiB_iBi​,问BiB_iBi​是否为AiA_iAi​的子序列。

0<n,m,q≤105,∑li≤1060<n,m,q\le 10^5,\sum l_i \le 10^60<n,m,q≤105,∑li​≤106

1≤max(Ai),max(Bij)≤m1\le max(A_i),max(B_{ij})\le m1≤max(Ai​),max(Bij​)≤m

# 做法1 - 二分查找

由于序列中的值的个数不会超过10510^5105,因此可以使用vector[i]来存放数字i(若数字的绝对值过大,离散化即可),当匹配BijB_{ij}Bij​时,通过二分查找下一个最小的xxx使得Ax=BijA_x=B_{ij}Ax​=Bij​

复杂度O(log(n)∑li)O(log(n)\sum l_i)O(log(n)∑li​)

# 做法2 - 可持久化线段树 (没必要)

朴素做法是使用数组next[i][j]next[i][j]next[i][j]表示AiA_iAi​之后,字符jjj的位置,匹配时不断跳转即可。

直接维护该数组的复杂度为O(字符集大小∗n)O(\text{字符集大小}*n)O(字符集大小∗n),显然不可。

观察可以发现,从next[i]→next[i−1]next[i]\to next[i-1]next[i]→next[i−1]的转移中,只有一个数据发生了变化,因而可以使用可持久化线段树维护该数组,空间复杂度O(log2(字符集大小)∗n)O(log_2(\text{字符集大小})*n)O(log2​(字符集大小)∗n),匹配的时间复杂度O(log2(字符集大小)∑li)O(log_2(\text{字符集大小})\sum l_i)O(log2​(字符集大小)∑li​)

Ps. 这里的字符集大小即题目中的mmm

# 做法3 - 思维 - 桶(离线) *

我们换个角度,让每一个BiB_iBi​去匹配AAA:给每个串一个指针PtriPtr_iPtri​(我们令AAA的指针为Ptr0Ptr_0Ptr0​,BiB_iBi​的指针为Ptri(i>0)Ptr_i(i>0)Ptri​(i>0)),一开始指针指向对应串的开头;不断向右移动指针Ptr0Ptr_0Ptr0​,若∗(ptr0)=∗(ptri)*(ptr_0)=*(ptr_i)∗(ptr0​)=∗(ptri​),则ptriptr_iptri​右移一位。

基本思路如上,但若是每次移动Ptr0Ptr_0Ptr0​都要扫一遍Ptri(1≤i≤q)Ptr_i(1\le i\le q)Ptri​(1≤i≤q),时间复杂度为O(nq)O(nq)O(nq),显然不行。

考虑到字符集大小mmm较小,我们可以使用桶来储存对应的指针:令Buck[x]Buck[x]Buck[x]内装所有∗(ptri)=x*(ptr_i)=x∗(ptri​)=x的指针,当∗(ptr0)=x*(ptr_0)=x∗(ptr0​)=x,我们令桶xxx内的所有指针+1,并将指针插入到对应的新桶中,接着清空桶xxx。


又因为∑li\sum l_i∑li​比较小,可以使用前向星来维护各个桶(见AC代码)。当然,也可以手动维护队列,稍微麻烦一点罢了。

AC代码

#include <bits/stdc++.h>
#define Android ios::sync_with_stdio(false), cin.tie(NULL)
using namespace std;

const int maxn = 1e5 + 100, maxl = 1e6 + 1000;

int head[maxn], nxt[maxl], val[maxl], vcnt = 0;
int ptr[maxn], ans[maxn];
vector<int> arr[maxn];

void add(int x, int v){
    nxt[vcnt] = head[x], val[vcnt] = v;
    head[x] = vcnt++;
}

void solve(){
    int n, q, m;
    cin >> n >> n >> q >> m;

    for(int i=0; i<=m; i++) head[i] = -1;

    arr[0].resize(n);
    for(int i=0; i<n; i++) cin >> arr[0][i];
    for(int i=1, l; i<=q; i++){
        cin >> l;
        arr[i].resize(l);
        for(int j=0; j<l; j++) cin >> arr[i][j];
        add(arr[i][0], i);
    }

    for(int i=0; i<n; i++){
        int v = arr[0][i], from = head[v];
        head[v] = -1; // 清空
        for(int p=from; ~p; p=nxt[p]){
            int k = val[p];
            ptr[k]++;
            if(ptr[k] == arr[k].size()){
                ans[k] = 1;
            }else{
                // 使用前向星,无需判断arr[k][ptr[k]]==arr[k][k-1]的情况
                add(arr[k][ptr[k]], k);
            }
        }
    }

    for(int i=1; i<=q; i++){
        cout << (ans[i]?"Yes":"No") << '\n';
    }
}
  
signed main(){
    Android;
    solve();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
上次更新: 2021/02/24, 03:37:30
回文树
字符串基础

← 回文树 字符串基础→

最近更新
01
深入理解Java虚拟机
03-04
02
DNS工作原理
02-27
03
改用VuePress啦
02-23
更多文章>
Theme by Vdoing | Copyright © 2019-2021
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式