洛谷 P2292 [HNOI2004] L 语言
题目描述
标点符号的出现晚于文字的出现,所以以前的语言都是没有标点的。现在你要处理的就是一段没有标点的文章。
一段文章 T T T 是由若干小写字母构成。一个单词 W W W 也是由若干小写字母构成。一个字典 D D D 是若干个单词的集合。我们称一段文章 T T T 在某个字典 D D D 下是可以被理解的,是指如果文章 T T T 可以被分成若干部分,且每一个部分都是字典 D D D 中的单词。
例如字典 D D D 中包括单词 is , name , what , your \texttt{is},\texttt{name},\texttt{what},\texttt{your} is,name,what,your,则文章 whatisyourname \texttt{whatisyourname} whatisyourname 是在字典 D D D 下可以被理解的,因为它可以分成 4 4 4 个单词: what , is , your , name \texttt{what},\texttt{is},\texttt{your},\texttt{name} what,is,your,name,且每个单词都属于字典 D D D,而文章 whatisyouname \texttt{whatisyouname} whatisyouname 在字典 D D D 下不能被理解,但可以在字典 D ′ = D ∪ { you } D'=D\cup\{\texttt{you}\} D′=D∪{you} 下被理解。这段文章的一个前缀 whatis \texttt{whatis} whatis,也可以在字典 D D D 下被理解,而且是在字典 D D D 下能够被理解的最长的前缀。
给定一个字典 D D D,你的程序需要判断若干段文章在字典 D D D 下是否能够被理解。并给出其在字典 D D D 下能够被理解的最长前缀的位置。
输入格式
第一行两个整数 n n n 和 m m m,表示字典 D D D 中有 n n n 个单词,且有 m m m 段文章需要被处理。
接下来 n n n 行,每行一个字符串 s s s,表示字典 D D D 中的一个单词。
接下来 m m m 行,每行一个字符串 t t t,表示一篇文章。
输出格式
对于输入的每一篇文章,你需要输出一行一个整数,表示这段文章在字典 D D D 可以被理解的最长前缀的位置。
样例 #1
样例输入 #1
4 3
is
name
what
your
whatisyourname
whatisyouname
whaisyourname
样例输出 #1
14
6
0
提示
样例 1 解释
- 对于第一个询问,整段文章
whatisyourname
都能被理解。 - 对于第二个询问,前缀
whatis
能够被理解。 - 对于第三个询问,没有任何前缀能够被理解。
数据规模与约定
- 对于 80 % 80\% 80% 的数据,保证 m ≤ 20 m \leq 20 m≤20, ∣ t ∣ ≤ 1 0 6 |t| \leq 10^6 ∣t∣≤106。
- 对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 20 1 \leq n \leq 20 1≤n≤20, 1 ≤ m ≤ 50 1 \leq m \leq 50 1≤m≤50, 1 ≤ ∣ s ∣ ≤ 20 1 \leq |s| \leq 20 1≤∣s∣≤20, 1 ≤ ∣ t ∣ ≤ 2 × 1 0 6 1 \leq |t| \leq 2 \times 10^6 1≤∣t∣≤2×106, s s s 与 t t t 中均只含小写英文字母。
提示
- 请注意数据读入对程序效率造成的影响。
- 请注意【数据规模与约定】中标注的串长是单串长度,并不是字符串长度和。
说明
本题数据有加强,其中前 80 % 80\% 80% 的数据为原测试数据。
思路
令
S
i
Si
Si表示文章的第
i
i
i个字母,
d
p
i
dpi
dpi表示文章的前
i
i
i位能否被理解(能就是1,不能即为0),那么我们可以知道,如果
S
i
S
i
+
1...
S
j
SiSi+1...Sj
SiSi+1...Sj能够匹配某个单词,那么
d
p
dp
dp
i
−
1
=
1
i-1 = 1
i−1=1,
d
p
j
=
1
dpj = 1
dpj=1.
但是数据范围不小,暴力枚举前面的那些字母去匹配是会超时的。看到多模式串,我们肯定得想到 AC 自动机喽。
但是经过套AC机后,我们只能必暴力解题多5分,我们就要开始进一步优化。
1.
1.
1.在算
d
p
i
dpi
dpi时,如果
d
p
i
dpi
dpi的值已经是一,那么我们就可以跳出循环
2.
2.
2.在算
d
p
i
dpi
dpi的时候,如果之前最多能匹配到的那一位离现在差的位数大于模式串长度的最大值,直接输出答案。
举个例子,如今在算第 100 位的答案,但是当前最多只能匹配到第
10 位,那么就没必要继续算了,答案就取 10。这是因为你至少需要长度为 90 的模式串才可能让当前长度能匹配,但是找不到这么长的模式串,所以再往后就更不可能了。
3.
3.
3.在跳失配数组的时候,其中很多对答案都没有贡献(即不在叶子结点的),所以我们在求失配数组时,可以再开个数组,把实际该跳到哪里给记录下来,这个做法在 P3796 [模板]AC 自动机(加强版)中有人提及。不过我的代码经过实测,不加该优化也可以 AC,下面放的代码也是没有这个优化的。当然,加上它应该会更快些。
对于AC自动机,我们可以先学Trie和KMP,这两个后续会出一期新文章去讲述。
#include<bits/stdc++.h>
#define ll long long
#define linf 0x3f3f3f3f3f3f3f3f
#define inf 0x7fffffff
#define INF 0x3f
#define v e[i].y
using namespace std;
inline ll read(){
char ch = getchar();
ll x = 0,w = 1;
while(ch < '0' || ch > '9'){
if(ch == '-') w = -1;
ch = getchar();
}
while(ch >= '0' && ch <= '9') x = (x << 1) + (x << 3) + ch - 48,ch = getchar();return w == 1?x:-x;
}
inline void write(ll x){
if(x < 0) x = -x,putchar('-');
if(x < 10){putchar(48 + x);return;}
write(x / 10),putchar((x + 10) % 10 + 48);
}
int n = read(),m = read(),q[100005],ans,h,t,maxl;
bool dp[2000005];
char xk[2000005];
struct AC{
int ch[30005][30],Fail[30005],val[30005],num;
void Insert(int L){
int op = 0;
for(int i = 1,j;i <= L;i ++){
j = xk[i] - 'a';
op=(ch[op][j]?ch[op][j]:ch[op][j] = ++ num);
}
val[op] = L,maxl = max(maxl,L);
}
void getfail(){
h = 0,t = 0;
for(int i = 0;i < 26;i ++) if(ch[0][i]) q[++ t] = ch[0][i],Fail[ch[0][i]] = 0;
while(h < t){
int u = q[++ h];
for(int i = 0;i < 26;i ++){
if(ch[u][i]) Fail[ch[u][i]] = ch[Fail[u]][i],q[++ t] = ch[u][i];
else ch[u][i] = ch[Fail[u]][i];
}
}
}
int solve(int L){
for(int i = 1;i <= L;i ++) dp[i] = 0;
dp[0] = 1,ans = 0;
int op = 0;
for(int i = 1,j;i <= L;i ++){
if(ans + maxl < i) break;
j = xk[i] - 'a',op = ch[op][j];
for(int k = op;k;k = Fail[k]){
dp[i] |= dp[i-val[k]];
if(dp[i]) break;
}
if(dp[i]) ans = i;
}
return ans;
}
}A;
int main(){
while(n --) scanf("%s",xk + 1),A.Insert(strlen(xk + 1));
A.getfail();
while(m --) scanf("%s",xk + 1),write(A.solve(strlen(xk + 1))),putchar(10);
return 0;
}