题目地址:
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∣
z∣x∣≤∣y∣,且
∀
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
0→A[0][a]=1→A[1][b]=2→A[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)。