字符串匹配
1. 概述
2. 例题
AcWing 831. KMP字符串
问题描述
- 问题链接:AcWing 831. KMP字符串
分析
-
注意:p和s的下标都是从 1 开始的。
-
具体分析如下图:
代码
- C++
#include <iostream>
using namespace std;
const int N = 100010, M = 1000010;
int n, m;
char s[M], p[N];
int ne[N];
int main() {
cin >> n >> p + 1 >> m >> s + 1;
// 求next数组, ne[1]=0表示如果p[1]没有匹配上,从头开始匹配
for (int i = 2, j = 0; i <= n; i++) {
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j++;
ne[i] = j;
}
// 匹配过程
for (int i = 1, j = 0; i <= m; i++) {
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j++;
if (j == n) {
printf("%d ", i - n);
j = ne[j];
}
}
return 0;
}
AcWing 3823. 寻找字符串
问题描述
- 问题链接:AcWing 3823. 寻找字符串
分析
-
KMP
中ne
数组的含义是模式串p
前缀等于后缀的最大长度。 -
因此本题可以将
s
看成模式串,求出s
对应的ne
数组,如果s
的长度为n
,则ne[n]
表示s
的前缀等于后缀的最大长度。 -
那如何枚举字符串
s
的所有前缀等于后缀的子串呢?根据KMP
的原理可知所有这些子串长度从大到小是:ne[n]、ne[ne[n]]、ne[ne[ne[n]]]、.....
,因此可以用循环枚举出这些子串的长度。 -
当枚举出某个前缀等于后缀的子串的长度后,如何判断中间是否出现这个子串呢?可以使用一个
bool
数组,st[k]
表示是否存在长度为k
的子串,其前缀等于后缀。 -
st
数组的求解:遍历[1~n)
之间的所有ne[i]
,让st[ne[i]]=true
即可,注意i
不能取到n
。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1000010;
int n;
char s[N];
int ne[N];
bool st[N];
int main() {
int T;
cin >> T;
while (T--) {
scanf("%s", s + 1);
n = strlen(s + 1);
for (int i = 2, j = 0; i <= n; i++) {
while (j && s[i] != s[j + 1]) j = ne[j];
if (s[i] == s[j + 1]) j++;
ne[i] = j;
}
for (int i = 1; i <= n; i++) st[i] = false;
for (int i = 1; i < n; i++) st[ne[i]] = true;
int res = 0;
for (int i = ne[n]; i; i = ne[i]) // 从大到小枚举所有前缀等于后缀的可能长度
if (st[i]) {
res = i;
break;
}
if (!res) puts("not exist");
else {
s[res + 1] = 0;
printf("%s\n", s + 1);
}
}
return 0;
}
AcWing 1052. 设计密码
问题描述
-
问题链接:AcWing 1052. 设计密码
分析
-
本题是状态机和
KMP
的结合。 -
假设串
T
的长度为m
,我们首先求出对于T
的next
数组,我们要得到一个长度为n
的串S
。 -
S
不包含T
的充要条件是:我们在字符串匹配的过程中无法匹配到T
,假设j
是当前T
中和S
中匹配的字母的数量,j
在T
中跳的过程中不能跳到m
,一旦跳到m
,则说明S
完全匹配到了T
,不满足题意。 -
因此关键在于理解:每一个合法的字符串,都可以一 一对应到一种KMP上的状态机的走法。
-
我们使用
f[i][j]
表示:当前考虑到S[i]
且在在状态机上走到位置j
的所有方案数。 -
我们可以依次枚举
S
中的每个位置,然后枚举当前状态机的位置j
,最后枚举该位置应该填哪个字母(对应图中的边),如果从该字母可以转移到状态机的位置u
,且u<m
,则可以用f[i][j]
更新状态f[i + 1][u]
。 -
本题中一共有
m + 1
种状态,因为j
的取值可以是0~m
。 -
本题存在两种扩展方式:
-
(1)需要不包含
k
个不同的字符串,对应题目:AcWing 1053. 修复DNA;解决方式:AC自动机、dp。 -
(2)数据量变大,
N
最大可以取到 1 0 9 10^9 109,不能包含的字符串只有一个,且长度不太大(几十左右),对应题目:AcWing 1305. GT考试。解决方式:kmp、dp、快速幂。
-
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 55, mod = 1e9 + 7;
int n, m;
char str[N];
int ne[N];
int f[N][N];
int main() {
cin >> n >> str + 1;
m = strlen(str + 1);
for (int i = 2, j = 0; i <= m; i++) {
while (j && str[i] != str[j + 1]) j = ne[j];
if (str[i] == str[j + 1]) j++;
ne[i] = j;
}
f[0][0] = 1;
for (int i = 0; i < n; i++) // 枚举目标串每一位
for (int j = 0; j < m; j++) // 枚举状态机中的每一个状态
for (char c = 'a'; c <= 'z'; c++) { // 枚举目标串中该位置填的字母(边)
// 枚举在位置j如果沿着c这条边走到k时,f[i, j]对f[i + 1, k]的方案数的贡献
int k = j;
while (k && c != str[k + 1]) k = ne[k];
if (c == str[k + 1]) k++;
if (k < m) f[i + 1][k] = (f[i + 1][k] + f[i][j]) % mod;
}
int res = 0; // 最后不走到m的状态都是
for (int i = 0; i < m; i++) res = (res + f[n][i]) % mod;
cout << res << endl;
return 0;
}
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
const int N = 55, mod = 1e9 + 7;
int n, m;
char str[N];
int ne[N];
int f[N][N];
vector<int> g[N]; // g[j]表示: f[i][j]是否可以转移到的位置f[i + 1][k]
int main() {
cin >> n >> str + 1;
m = strlen(str + 1);
// kmp
for (int i = 2, j = 0; i <= m; i++) {
while (j && str[i] != str[j + 1]) j = ne[j];
if (str[i] == str[j + 1]) j++;
ne[i] = j;
}
// 预处理建图
for (int j = 0; j < m; j++) // 枚举状态机中的每一个状态
for (char c = 'a'; c <= 'z'; c++) { // 枚举目标串中该位置填的字母(边)
// 枚举在位置j如果沿着c这条边走到k时,f[i, j]对f[i + 1, k]的方案数的贡献
int k = j;
while (k && c != str[k + 1]) k = ne[k];
if (c == str[k + 1]) k++;
if (k < m) g[j].push_back(k);
}
// 根据状态机进行递推
f[0][0] = 1;
for (int i = 0; i < n; i++) // 枚举目标串每一位
for (int j = 0; j < m; j++) { // 枚举状态机中的每一个状态
for (auto k : g[j])
f[i + 1][k] = (f[i + 1][k] + f[i][j]) % mod;
}
// 计算最终答案
int res = 0; // 最后不走到m的状态都是
for (int i = 0; i < m; i++) res = (res + f[n][i]) % mod;
cout << res << endl;
return 0;
}
AcWing 1053. 修复DNA
问题描述
-
问题链接:AcWing 1053. 修复DNA
分析
-
本题需要使用AC自动机解决,关于AC自动机的原理可以参考:AC自动机,这里使用AC自动机的优化版本:trie树。需要修改的串记为
str
。 -
状态表示:
f(i, j)
:当前考虑到str[i]
,并且走到了AC自动机中的第j
个位置的所有合法操作方案中(不包含病毒串),最少修改的字母数量。 -
这里的思路是枚举出所有合法的串,对比和原串不同的字母数量,取一个最小值。
-
这里
AGCT
分别使用0、1、2、3
代替。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010, INF = 0x3f3f3f3f;
int n, m;
int tr[N][4], idx;
bool dar[N]; // trie中不能走到的节点
int q[N], ne[N];
char str[N]; // 待修复的DNA片段
int f[N][N]; // dp数组
int get(char c) {
if (c == 'A') return 0;
if (c == 'T') return 1;
if (c == 'G') return 2;
return 3;
}
// 向trie中插入字符串
void insert() {
int p = 0;
for (int i = 0; str[i]; i++) {
int u = get(str[i]);
if (!tr[p][u]) tr[p][u] = ++idx;
p = tr[p][u];
}
dar[p] = true;
}
// 构建trie图
void build() {
int hh = 0, tt = -1;
for (int i = 0; i < 4; i++)
if (tr[0][i])
q[++tt] = tr[0][i];
while (hh <= tt) {
int t = q[hh++];
for (int i = 0; i < 4; i++) {
int p = tr[t][i]; // t的孩子节点p
if (!p) tr[t][i] = tr[ne[t]][i];
else {
ne[p] = tr[ne[t]][i];
q[++tt] = p;
// ne[p]节点不合法,则说明从根节点到ne[p]是非法字符串
// 则以p节点结尾的后缀也不合法
dar[p] |= dar[ne[p]];
}
}
}
}
int main() {
int T = 1;
while (scanf("%d", &n), n) {
memset(tr, 0, sizeof tr);
memset(dar, 0, sizeof dar);
memset(ne, 0, sizeof ne);
idx = 0;
// 建立trie
for (int i = 0; i < n; i++) {
scanf("%s", str);
insert();
}
// 建立AC自动机
build();
scanf("%s", str + 1);
m = strlen(str + 1);
memset(f, 0x3f, sizeof f);
f[0][0] = 0;
for (int i = 0; i < m; i++) // 枚举目标串每一位
for (int j = 0; j <= idx; j++) // 枚举状态机中的每一个状态
for (int c = 0; c < 4; c++) { // 枚举目标串中该位置填的字母(边)
// 枚举在位置j如果沿着c这条边走到k时,f[i, j]可以走到f[i + 1, k]
int k = tr[j][c];
int t = get(str[i + 1]) != c; // 这里填写的字母和原串中的字母不同的话,需要修改一次
if (!dar[k]) // 可以到达状态机的这个位置
f[i + 1][k] = min(f[i + 1][k], f[i][j] + t);
}
int res = INF;
for (int i = 0; i <= idx; i++) res = min(res, f[m][i]);
if (res == INF) res = -1;
printf("Case %d: %d\n", T++, res);
}
return 0;
}
AcWing 1305. GT考试
问题描述
-
问题链接:AcWing 1305. GT考试
分析
-
这一题是AcWing 1052. 设计密码的一道扩展题目,分析方式仍然是动态规划。扩展方式是数据量,AcWing 1052. 设计密码中的
n
值最大为50,这里的n
最大可以取到 1 0 9 10 ^ 9 109。这是一种扩展方式,还有另外一种扩展方式,不扩展n
,而是让不能包含多个字符串,对应题目是:AcWing 1053. 修复DNA,可以使用AC自动机解决。 -
本题的分析如下:
- 通过上面的分析,我们根据状态计算可以得到第
i
层和第i+1
层之间的关系,即
f ( i + 1 , 0 ) = a 0 , 0 × f ( i , 0 ) + a 1 , 0 × f ( i , 1 ) + . . . + a m − 1 , 0 × f ( i , m − 1 ) f ( i + 1 , 1 ) = a 0 , 1 × f ( i , 0 ) + a 1 , 1 × f ( i , 1 ) + . . . + a m − 1 , 1 × f ( i , m − 1 ) . . . f ( i + 1 , m − 1 ) = a 0 , m − 1 × f ( i , 0 ) + a 1 , m − 1 × f ( i , 1 ) + . . . + a m − 1 , m − 1 × f ( i , m − 1 ) f(i+1, 0) = a_{0,0} \times f(i, 0) + a_{1,0} \times f(i, 1) + ... + a_{m-1,0} \times f(i, m - 1) \\ f(i+1, 1) = a_{0,1} \times f(i, 0) + a_{1,1} \times f(i, 1) + ... + a_{m-1,1} \times f(i, m - 1) \\ ... \\ f(i+1, m-1) = a_{0,m-1} \times f(i, 0) + a_{1,m-1} \times f(i, 1) + ... + a_{m-1,m-1} \times f(i, m - 1) f(i+1,0)=a0,0×f(i,0)+a1,0×f(i,1)+...+am−1,0×f(i,m−1)f(i+1,1)=a0,1×f(i,0)+a1,1×f(i,1)+...+am−1,1×f(i,m−1)...f(i+1,m−1)=a0,m−1×f(i,0)+a1,m−1×f(i,1)+...+am−1,m−1×f(i,m−1)
如果我们令:
F
(
i
+
1
)
=
[
f
(
i
+
1
,
0
)
,
f
(
i
+
1
,
1
)
,
.
.
.
,
f
(
i
+
1
,
m
−
1
)
]
A
=
[
a
0
,
0
a
0
,
1
.
.
.
a
0
,
m
−
1
a
1
,
0
a
1
,
1
.
.
.
a
1
,
m
−
1
.
.
.
.
.
.
.
.
.
.
.
.
a
m
−
1
,
0
a
m
−
1
,
1
.
.
.
a
m
−
1
,
m
−
1
]
F(i+1) = [f(i+1, 0), f(i+1, 1), ..., f(i+1, m-1)] \\ A = \left[ \begin{matrix} a_{0,0} & a_{0,1} & ... & a_{0,m-1} \\ a_{1,0} & a_{1,1} & ... & a_{1,m-1} \\ ... & ... & ... & ... \\ a_{m-1,0} & a_{m-1,1} & ... & a_{m-1,m-1} \end{matrix} \right]
F(i+1)=[f(i+1,0),f(i+1,1),...,f(i+1,m−1)]A=⎣⎢⎢⎡a0,0a1,0...am−1,0a0,1a1,1...am−1,1............a0,m−1a1,m−1...am−1,m−1⎦⎥⎥⎤
则有:
F
(
i
+
1
)
=
F
(
i
)
×
A
F(i+1) = F(i) \times A
F(i+1)=F(i)×A
展开为:
[
f
(
i
+
1
,
0
)
,
f
(
i
+
1
,
1
)
,
.
.
.
,
f
(
i
+
1
,
m
−
1
)
]
=
[
f
(
i
,
0
)
,
f
(
i
,
1
)
,
.
.
.
,
f
(
i
,
m
−
1
)
]
×
[
a
0
,
0
a
0
,
1
.
.
.
a
0
,
m
−
1
a
1
,
0
a
1
,
1
.
.
.
a
1
,
m
−
1
.
.
.
.
.
.
.
.
.
.
.
.
a
m
−
1
,
0
a
m
−
1
,
1
.
.
.
a
m
−
1
,
m
−
1
]
[f(i+1, 0), f(i+1, 1), ..., f(i+1, m-1)] = \\ [f(i, 0), f(i, 1), ..., f(i, m-1)] \times \left[ \begin{matrix} a_{0,0} & a_{0,1} & ... & a_{0,m-1} \\ a_{1,0} & a_{1,1} & ... & a_{1,m-1} \\ ... & ... & ... & ... \\ a_{m-1,0} & a_{m-1,1} & ... & a_{m-1,m-1} \end{matrix} \right]
[f(i+1,0),f(i+1,1),...,f(i+1,m−1)]=[f(i,0),f(i,1),...,f(i,m−1)]×⎣⎢⎢⎡a0,0a1,0...am−1,0a0,1a1,1...am−1,1............a0,m−1a1,m−1...am−1,m−1⎦⎥⎥⎤
- 根据上面的分析可知,矩阵
A
只与不合法串S
有关,因此A
矩阵是不变的。根据上面递推式可知:
F ( n ) = F ( 0 ) × A n F ( 0 ) = [ 1 , 0 , 0 , . . . ] F(n) = F(0) \times A ^{n} \quad \quad F(0) = [1, 0, 0, ...] F(n)=F(0)×AnF(0)=[1,0,0,...]
- 如何求解数组
A
呢?如果从f(i, j)
可以转移到f(i+1, k)
,则让a[j, k]++
。即让f(i+1, k) += f(i, j)
:
f ( i + 1 , k ) = a 0 , k × f ( i , 0 ) + a 1 , k × f ( i , 1 ) + . . . + a j , k × f ( i , j ) + . . . + a m − 1 , k × f ( i , m − 1 ) f(i+1, k) = a_{0,k} \times f(i, 0) + a_{1,k} \times f(i, 1) +... + a_{j,k} \times f(i, j) + ... + a_{m-1,k} \times f(i, m - 1) f(i+1,k)=a0,k×f(i,0)+a1,k×f(i,1)+...+aj,k×f(i,j)+...+am−1,k×f(i,m−1)
- 求出向量
F(n)
后,最后的答案就是向量F(n)
中所有的元素之和。 - 这是一类问题,凡是动态规划中两层之间的转移形式是乘以一个固定矩阵的,都可以使用快速幂优化。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 25;
int n, m, mod; // 准考证号为 n 位数, 不吉利数字为m位
char str[N]; // 不吉利数字串
int ne[N]; // KMP求str自身的ne
int a[N][N]; // 转移矩阵
void mul(int c[][N], int a[][N], int b[][N]) {
static int t[N][N];
memset(t, 0, sizeof t);
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
for (int k = 0; k < N; k++)
t[i][j] = (t[i][j] + a[i][k] * b[k][j]) % mod;
memcpy(c, t, sizeof t);
}
int qmi(int k) {
int f0[N][N] = {1};
while (k) {
if (k & 1) mul(f0, f0, a); // f0 = f0 * a
mul(a, a, a); // a = a * a;
k >>= 1;
}
int res = 0;
for (int i = 0; i < m; i++) res = (res + f0[0][i]) % mod;
return res;
}
int main() {
cin >> n >> m >> mod;
cin >> str + 1;
// KMP
for (int i = 2, j = 0; i <= m; i++) {
while (j && str[i] != str[j + 1]) j = ne[j];
if (str[i] == str[j + 1]) j++;
ne[i] = j;
}
// 初始化A[i][j]
for (int j = 0; j < m; j++)
for (int c = '0'; c <= '9'; c++) {
int k = j; // 原字符串后缀和str前缀匹配的长度
while (k && str[k + 1] != c) k = ne[k];
if (str[k + 1] == c) k++;
if (k < m) a[j][k]++;
}
// F[n] = F[0] * A^n
cout << qmi(n) << endl;
return 0;
}
AcWing 1282. 搜索关键词
问题描述
-
问题链接:AcWing 1282. 搜索关键词
分析
-
本题中的步骤是:
(1)将所有单词存入到trie树中;
(2)然后在trie树上求解next数组;
(3)匹配过程:trie树中的单词匹配文章。
-
对于第(3)步,我们需要注意,对于当前trie树中匹配到的字符串,需要将其最大后缀对应的字符串个数都加上(我们不需要考虑当前字符串是否为输入放入单词,因为不是单词的话,节点中对应的cnt值为0)。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 10010, S = 55, M = 1000010;
int n; // 单词数量
int tr[N * S][26];
int cnt[N * S]; // 以每个节点结尾的单词的数量
int idx;
char str[M]; // 读取输入字符串
int q[N * S]; // BFS求ne数组时的队列
int ne[N * S];
// trie中的插入函数
void insert() {
int p = 0; // 0既代表根节点,也代表空节点
for (int i = 0; str[i]; i++) {
int t = str[i] - 'a';
if (!tr[p][t]) tr[p][t] = ++idx;
p = tr[p][t];
}
cnt[p]++;
}
void build() {
int hh = 0, tt = -1;
// 第一层、第二层对应的ne值都为0,直接将第二层入队即可
for (int i = 0; i < 26; i++)
if (tr[0][i]) // 根节点0存在孩子i
q[++tt] = tr[0][i];
while (hh <= tt) {
int t = q[hh++];
for (int i = 0; i < 26; i++) {
int c = tr[t][i];
if (!c) continue;
int j = ne[t];
while (j && !tr[j][i]) j = ne[j];
if (tr[j][i]) j = tr[j][i];
ne[c] = j;
q[++tt] = c;
}
}
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
memset(tr, 0, sizeof tr);
memset(cnt, 0, sizeof cnt);
memset(ne, 0, sizeof ne);
idx = 0;
// (1) 建立trie树
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%s", str);
insert();
}
// (2) 在trie树上求解next数组
build();
// (3) 匹配过程:trie树中的单词匹配文章
scanf("%s", str);
int res = 0; // 表示匹配的单词的数量
for (int i = 0, j = 0; str[i]; i++) { // 遍历文章中的每个字符
int t = str[i] - 'a';
while (j && !tr[j][t]) j = ne[j];
if (tr[j][t]) j = tr[j][t];
int p = j;
while (p) {
res += cnt[p];
cnt[p] = 0; // 该单词如果出现过,统一一遍即可
p = ne[p];
}
}
printf("%d\n", res);
}
return 0;
}
// trie图
#include <iostream>
#include <cstring>
using namespace std;
const int N = 10010, S = 55, M = 1000010;
int n; // 单词数量
int tr[N * S][26];
int cnt[N * S]; // 以每个节点结尾的单词的数量
int idx;
char str[M]; // 读取输入字符串
int q[N * S]; // BFS求ne数组时的队列
int ne[N * S];
// trie中的插入函数
void insert() {
int p = 0; // 0既代表根节点,也代表空节点
for (int i = 0; str[i]; i++) {
int t = str[i] - 'a';
if (!tr[p][t]) tr[p][t] = ++idx;
p = tr[p][t];
}
cnt[p]++;
}
void build() {
int hh = 0, tt = -1;
// 第一层、第二层对应的ne值都为0,直接将第二层入队即可
for (int i = 0; i < 26; i++)
if (tr[0][i]) // 根节点0存在孩子i
q[++tt] = tr[0][i];
while (hh <= tt) {
int t = q[hh++];
for (int i = 0; i < 26; i++) {
int &p = tr[t][i];
if (!p) p = tr[ne[t]][i]; // 不存在到i的边
else {
ne[p] = tr[ne[t]][i];
q[++tt] = p;
}
}
}
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
memset(tr, 0, sizeof tr);
memset(cnt, 0, sizeof cnt);
memset(ne, 0, sizeof ne);
idx = 0;
// (1) 建立trie树
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%s", str);
insert();
}
// (2) 在trie树上求解next数组
build();
// (3) 匹配过程:trie树中的单词匹配文章
scanf("%s", str);
int res = 0; // 表示匹配的单词的数量
for (int i = 0, j = 0; str[i]; i++) { // 遍历文章中的每个字符
int t = str[i] - 'a';
j = tr[j][t];
int p = j;
while (p) {
res += cnt[p];
cnt[p] = 0; // 该单词如果出现过,统一一遍即可
p = ne[p];
}
}
printf("%d\n", res);
}
return 0;
}
AcWing 1285. 单词
问题描述
-
问题链接:AcWing 1285. 单词
分析
-
每一行都是一个单词,需要在所有给定的单词中进行匹配,可以认为模板串和待匹配的串是同一组数据。
-
对于所以输入的单词,建立一棵trie树,对于某个单词我们想要统计出其在其他所有(包含自己)串中出现的次数,我们应该怎么统计呢?
-
首先,我们要明确某个单词出现的次数一定小于等于所有字符串的总长度。
-
一个字符串出现的次数=所有满足要求的前缀个数,要求是这个前缀的的后缀等于原串。
-
这里采用另外一种思路:考虑每一个前缀t,t的后缀等于多少个前缀。相当于反过来考虑,这样一来我们可以迭代求解。
-
对于所有存在的边(i, next[i]),我们都连一条边,则我们会形成一个有向无环图,因为i所在的层一定比next[i]所在的层深。
-
上图中f的含义:
(1)建立trie后, f代表当前节点代表的字符串(必须从根节点开始形成的字符串)出现的次数;
(2)依据拓扑序进行递推即可,即
f[next[i]]+=f[i]
。递推之后, f代表当前节点代表的字符串在整个trie中出现的次数。这个递推过程可以参考上面的原理中的图,如下图:
代码
- C++
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n; // 单词个数
int tr[N][26], idx;
// 建立trie后, f代表当前节点代表的字符串(必须从根节点开始形成的字符串)出现的次数
// 递推之后, f代表当前节点代表的字符串在整个trie中出现的次数
int f[N];
int q[N]; // BFS求ne时使用到的队列
int ne[N];
char str[N]; // 输入字符串
int id[210]; // 每个单词在trie中对应节点的编号
void insert(int x) {
int p = 0;
for (int i =0; str[i]; i++) {
int t = str[i] - 'a';
if (!tr[p][t]) tr[p][t] = ++idx;
p = tr[p][t];
f[p]++; // 每一个结束的位置都代表一个字符串
}
id[x] = p;
}
void build() {
int hh = 0, tt = -1;
for (int i = 0; i < 26; i++)
if (tr[0][i])
q[++tt] = tr[0][i];
while (hh <= tt) {
int t = q[hh++];
for (int i = 0; i < 26; i++) {
int &p = tr[t][i];
if (!p) p = tr[ne[t]][i];
else {
ne[p] = tr[ne[t]][i];
q[++tt] = p;
}
}
}
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%s", str);
insert(i); // i是当前单词对应编号
}
// 求解ne
build();
// 递推更新f, trie中节点编号为0~idx,一共idx+1个点,0既代表根节点又代表空节点
for (int i = idx; i; i--) f[ne[q[i]]] += f[q[i]];
for (int i = 0; i < n; i++) printf("%d\n", f[id[i]]);
return 0;
}