【可持久化线段树】【P5826】【模板】子序列自动机
Description
给定一个序列 (A),有 (q) 次询问,每次询问一个序列 (B) 是不是 (A) 的子序列
Limitations
序列 (A) 长度不超过 (10^5),询问序列长度之和不超过 (10^6),询问次数不超过 (10^5)
Solution
题外话:有关这道题的难度,我觉得大概到不了紫色,但是可持久化线段树的板子是紫色的,所以就设成了紫色
Algorithm (1)
考虑对于一个询问序列 (B),设其与 (A) 的最长公共子序列在 (A) 中的下标序列为 (Z),显然当且仅当 (Z) 的长度为 (|B|) 时,(B) 是 (A) 的子序列。合法的序列 (Z) 可能会有多个,但是只要我们找到了字典序最小的长度为 (|B|) 的序列 (Z),就可以说明 (B) 是 (A) 的子序列,否则不是。
考虑寻找字典序最小的 (Z) 可以贪心的选择,即对于 (B) 的每个前缀,可以求出其对应的 (Z) 序列的最后一位最小是多少,当 (B) 的前缀新增一个数字时,只需要在 (A) 中从当前 (Z) 序列最后一位的值的位置开始继续向后扫描,扫到第一个等于新增数字的位置,即是新的 (Z) 序列的最后一位。而如果扫描到了 (A) 的最后也没有找到,则意味着不存在合法的 (Z) 序列,因此 (B) 不是 (A) 的子序列。
这样的话每次询问时,最多扫描 (A) 一次,因此总时间复杂度为 (O(nq + sum L)),可以通过 Subtask (1),期望得分 (20~pts)
Algorithm (2)
考虑对 (A) 建立一个子序列自动机,用来识别 (A) 的所有子序列。
同样运用 Algorithm 1
中的思想,对于一个字符串(B),我们只要找到了其与 (A) 的最长公共子序列在 (A) 中的字典序最小的下标序列 (Z),就可以说明 (B) 是 (A) 的子序列。那么对于 (A) 的每一位而言,在其需要新匹配一个数字时,应该转移到 (A) 后面第一个为该数字的位置,显然这样才能保证 (Z) 序列的字典序是最小的。因此我们的转移应该对每个位置维护加入一个数字以后它后面第一个为该数字的位置。
考虑我们对 (A) 从后向前逐位建立自动机,对于第 (i) 位而言,第 (i - 1) 位加入 (A_i) 应该转移到 (i),而加入其它数字应该转移到 (A_i) 加入该数字后转移到的位置。因此有伪代码
for i : m do
trans[n][i] <- -1
end
for i = n : 1 do
for j = 1 : m do
trans[i - 1][j] <- trans[i][j]
end
trans[i - 1][A[i]] <- i
end
其中 (n) 代表 (A) 的长度,(m) 代表 (A) 中的最大值,(trans) 是一个二维数组,代表这个自动机。
而对一个字符串 (B) 进行匹配时,只需要将 (B) 顺着自动机的转移跑一遍,若没有跑出自动机,则 (B) 是 (A) 的子序列,否则不是。
Function check:
pos <- 0
ret <- true
for i = 1 : L do
pos <- trans[pos][B[i]]
if pos == -1 then
ret <- false
break
endif
end
return ret
end Func
注意到这样构造自动机的时间复杂度为 (O(nm)),匹配的复杂度为 (O(sum L)),因此总时间复杂度 (O(nm + sum L)),可以通过 Subtask (1),(2),期望得分 (55~pts)。
Algorithm (3)
注意到构造自动机时,第 (i) 位与第 (i - 1) 位只有 (A_i) 一项不一样,第 (i - 1) 位的转移可以看做第 (i) 位的转移的基础上修改了一个位置,因此我们可以从后向前使用可持久化线段树来维护每个位置的转移数组,这样建立自动机的时间复杂度为 (O(n log m)),匹配的时间复杂度为 (O(sum L log m))。总时间复杂度 (O((n + sum L) log m)),可以通过全部的 Subtask,期望得分 (100~pts)。
Code
Algorithm (2)
代码来自 @_皎月半洒花
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#define MAXN 200010
using namespace std ;
int L, N, M, Q, S[MAXN], nxt[MAXN][102] ;
void build(){
for (int i = 1 ; i <= M ; ++ i)
nxt[L + 2][i] = nxt[L + 1][i] = L + 2 ;
for (int i = L ; i ; -- i)
memcpy(nxt[i - 1], nxt[i], sizeof(nxt[i])), nxt[i - 1][S[i]] = i ;
}
int qr(){
char c = getchar() ;
int res = 0 ; while (!isdigit(c)) c = getchar() ;
while (isdigit(c)) res = (res << 1) + (res << 3) + c - 48, c = getchar() ;
return res ;
}
int main(){
int i, j, k, emm ;
cin >> emm >> N >> Q >> M ; L = N ;
for (i = 1 ; i <= L ; ++ i) scanf("%d", &S[i]) ; build() ;
for (i = 1 ; i <= Q ; ++ i){
N = qr() ; int st = 0, ans = 0 ;
for (j = 1 ; j <= N ; ++ j){
k = qr(), st = nxt[st][k] ;
if (!st){
while (j < N)
++ j, emm = qr() ;
ans = 1 ;
}
// cout << st << endl ;
}
printf(ans ? "No
" : "Yes
") ;
}
return 0 ;
}
Algorithm (3)
#include <cstdio>
template <typename T>
inline void qr(T &x) {
char ch;
do ch = getchar(); while ((ch > '9') || (ch < '0'));
do x = x * 10 + (ch ^ 48), ch = getchar(); while ((ch >= '0') && (ch <= '9'));
}
const int maxn = 100005;
struct Tree {
Tree *ls, *rs;
int l, r, v;
Tree(const int L, const int R) : l(L), r(R), v(-1) {
if (l != r) {
int mid = (l + r) >> 1;
ls = new Tree(l, mid);
rs = new Tree(mid + 1, r);
}
}
Tree(Tree *pre, const int P, const int V) : l(pre->l), r(pre->r), v(0) {
if (l == r) {
v = V;
} else {
if (pre->ls->r >= P) {
rs = pre->rs;
ls = new Tree(pre->ls, P, V);
} else {
ls = pre->ls;
rs = new Tree(pre->rs, P, V);
}
}
}
int query(const int x) {
if (this->l == this->r) {
return this->v;
} else {
return (this->ls->r >= x) ? this->ls->query(x) : this->rs->query(x);
}
}
};
Tree *rot[maxn];
int tp, n, q, m;
int MU[maxn];
int main() {
qr(tp); qr(n); qr(q); qr(m);
rot[n] = new Tree(1, m);
for (int i = 1; i <= n; ++i) {
qr(MU[i]);
}
for (int i = n; i; --i) {
rot[i - 1] = new Tree(rot[i], MU[i], i);
}
for (int L, x, pos; q; --q) {
L = pos = 0; qr(L);
while ((L--) && (pos != -1)) {
x = 0; qr(x);
if ((pos = rot[pos]->query(x)) == -1) {
while (L--) {
qr(x);
}
break;
}
}
puts((~pos) ? "Yes" : "No");
}
return 0;
}
appreciation
感谢验题人:@_皎月半洒花 @water_lift
感谢本文的审核与校对:@Dusker