【算法专题】字符串匹配

字符串匹配

1. 概述

  • 字符串匹配的经典算法是KMP,关于KMP可以参考:网址。还会用到AC自动机,可以参考:AC自动机

  • 这里不仅会讨论字符串匹配的情况,还会讨论不能含有某些字符串的情况。

2. 例题

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. 寻找字符串

问题描述

在这里插入图片描述

分析

  • KMPne数组的含义是模式串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. 设计密码

问题描述

分析

  • 本题是状态机和KMP的结合。

  • 假设串T的长度为m,我们首先求出对于Tnext数组,我们要得到一个长度为n的串S

  • S不包含T的充要条件是:我们在字符串匹配的过程中无法匹配到T,假设j是当前T中和S中匹配的字母的数量,jT中跳的过程中不能跳到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

问题描述

分析

  • 本题需要使用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 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)+...+am1,0×f(i,m1)f(i+1,1)=a0,1×f(i,0)+a1,1×f(i,1)+...+am1,1×f(i,m1)...f(i+1,m1)=a0,m1×f(i,0)+a1,m1×f(i,1)+...+am1,m1×f(i,m1)

如果我们令:
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,m1)]A=a0,0a1,0...am1,0a0,1a1,1...am1,1............a0,m1a1,m1...am1,m1
则有:
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,m1)]=[f(i,0),f(i,1),...,f(i,m1)]×a0,0a1,0...am1,0a0,1a1,1...am1,1............a0,m1a1,m1...am1,m1

  • 根据上面的分析可知,矩阵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)+...+am1,k×f(i,m1)

在这里插入图片描述

  • 求出向量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. 搜索关键词

问题描述

分析

  • 本题中的步骤是:

    (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. 单词

问题描述

分析

  • 每一行都是一个单词,需要在所有给定的单词中进行匹配,可以认为模板串和待匹配的串是同一组数据。

  • 对于所以输入的单词,建立一棵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;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值