• 字典树、AC自动机、后缀数组


    目录

    字典树

    AC自动机

    后缀数组


            在学习的过程中,不懂的可以先将功能代码作为API,首先懂得如何调用,懂得各个特定数组的定义,先会灵活使用即可。

    字典树

            字典树,顾名思义,这里以26个小写字母为例。特别地,字典树的根节点,字符内容为空。而对于字典树上的每一个节点(包括根节点),都有26个孩子节点。

    这里需要非常清楚 trie[cur][i] 数组的含义:代表编号为cur的节点的第i个孩子的节点。 

    题目概述:

            给n个字符串s1...sn,  再给出一个字符串p,问s1...sn中,前缀为p的字符串有多少个?

    1. // problem :
    2. #include
    3. using namespace std;
    4. #define ll long long
    5. typedef pair<int, int> PII;
    6. #define pb push_back
    7. char s[15];
    8. int trie[1000010][26];
    9. int val[1000010];
    10. int sz = 0;
    11. void insert(char s[]) {
    12. int size = strlen(s), cur = 0;
    13. for (int i = 0; i < size; ++i) {
    14. int v = s[i] - 'a';
    15. if (trie[cur][v] == 0)
    16. trie[cur][v] = ++sz;
    17. cur = trie[cur][v];
    18. val[cur]++;
    19. }
    20. }
    21. int query(char s[]) {
    22. int size = strlen(s), cur = 0;
    23. for (int i = 0; i < size; ++i) {
    24. int v = s[i] - 'a';
    25. if (trie[cur][v] == 0) return 0;
    26. cur = trie[cur][v];
    27. }
    28. return val[cur];
    29. }
    30. int main(){
    31. int n; scanf("%d", &n);
    32. for (int i = 1; i <= n; ++i) {
    33. scanf("%s", s);
    34. insert(s);
    35. }
    36. scanf("%s", s);
    37. printf("%d\n", query(s));
    38. return 0;
    39. }

    AC自动机

            KMP是单模式匹配文本算法,其中关键是nxt数组。核心思想是:最大限度地利用先前已经匹配的字符,从而避免无效字符的多次匹配

            AC自动机是多模式匹配文本算法,其中的关键是fail数组,核心思想同KMP一样。将多个字符串构建成一颗字典树,在字典树上跑“KMP”, 其实AC自动机跟KMP没什么联系,只是思想上是差不多的。 

    上图为  abcd、abd、bcd、cd四个字符串建立的字典树。

            比如文本串为abcf, 在匹配到abcd中的c之后,发现f不匹配,根据fail数组,我们将原本匹配的abc变化为“已经”匹配好的bc,从而对文本匹配的过程进行了加速。所以关键是fail数组如何去构建。该过程由BFS完成。

    题目链接:Keywords Search Problem - 2222 (hdu)

    题目概述:

            给出一个文本串和多个模式串,求该文本串中包含多少种模式串?

    题目分析:

            求多少种,所以,在匹配完一个模式串之后,将对应的val值置为-1,如果下次遇到val = -1的点,就跳过。(不跳过也行,但就相当于没有一个很好的剪枝操作, 可能导致时间超限, 需要注意的是,memset在数组大的时候,也是一个很耗时的操作,没必要将所有的数组元素memset)

    1. // problem :
    2. #include
    3. using namespace std;
    4. #define ll long long
    5. typedef pair<int, int> PII;
    6. #define pb push_back
    7. // AC自动机
    8. const int N = 1001005;
    9. int trie[N][26], val[N], fail[N], sz;
    10. queue<int> q;
    11. struct AC_Automaton {
    12. AC_Automaton() {
    13. sz = 0;
    14. memset(trie[0], 0, sizeof(trie[0]));
    15. }
    16. void node_clear(int x) {
    17. memset(trie[x], 0, sizeof(trie[x]));
    18. val[x] = fail[x] = 0;
    19. }
    20. void insert (char s[]) {
    21. int len = strlen(s + 1), cur = 0;
    22. for (int i = 1; i <= len; ++i) {
    23. int v = s[i] - 'a';
    24. if (!trie[cur][v]) {
    25. trie[cur][v] = ++sz;
    26. node_clear(sz);
    27. }
    28. cur = trie[cur][v];
    29. }
    30. val[cur]++;
    31. }
    32. void get_fail() {
    33. for (int i = 0; i < 26; ++i) if (trie[0][i]) {
    34. fail[trie[0][i]] = 0;
    35. q.push(trie[0][i]);
    36. }
    37. while (!q.empty()) {
    38. int cur = q.front(); q.pop();
    39. for (int i = 0; i < 26; ++i) {
    40. if (trie[cur][i]) fail[trie[cur][i]] = trie[fail[cur]][i], q.push(trie[cur][i]);
    41. else trie[cur][i] = trie[fail[cur]][i];
    42. }
    43. }
    44. }
    45. int query(char s[]) {
    46. int len = strlen(s + 1), cur = 0, ans = 0;
    47. for (int i = 1; i <= len; ++i) {
    48. cur = trie[cur][s[i] - 'a'];
    49. for (int t = cur; t && ~val[t]; t = fail[t]) {
    50. ans += val[t];
    51. val[t] = -1;
    52. }
    53. }
    54. return ans;
    55. }
    56. };
    57. // 读入
    58. int n;
    59. char s[N];
    60. void solve() {
    61. scanf("%d", &n);
    62. AC_Automaton AC;
    63. for (int i = 1; i <= n; ++i) {
    64. scanf("%s", s + 1);
    65. AC.insert(s);
    66. }
    67. AC.get_fail();
    68. scanf("%s", s + 1);
    69. int ans = AC.query(s);
    70. printf("%d\n", ans);
    71. }
    72. int main(){
    73. int t; scanf("%d", &t);
    74. for (int i = 1; i <= t; ++i) {
    75. solve();
    76. }
    77. return 0;
    78. }

    后缀数组

    以例子来介绍后缀数组。

    例如有个字符串为ababc,则其后缀字符串为:

     abaca

       baca

         aca

           ca

             a

    将后缀字符串按字典序从小到大进行排序后,得到:

    a

    abaca

    aca

    baca

    ca

    后缀数组中,两个至关重要的是 sa数组,以及height数组。还有一个数组结构为rnk数组

    sa[i] : 字典序排名第i的后缀字符串是排序前的第sa[i]个字符串。

    height[i] :  排序后的第i个字符串和第i - 1个字符串的最大匹配前缀是height[i] 

    rnk[i] : 与sa[i]相反,排序前的第i个字符串在排序后排名第几

    由sa数组的定义可知,排序后的第i个字符串在初始串的下标,为sa[i]

    以上述的例子为例,求出各数组的值:

    i    排序前   排序后    sa     rnk    height

    1   abaca    a              5       2          0

    2     baca    abaca      1       4          1

    3       aca    aca          3       3          1

    4         ca    baca        2       5          0

    5           a    ca            4       1          0

    求sa、height、rnk数组的模板为:

    1. const int N = 200005;
    2. int wa[N],wb[N],wv[N],wss[N];
    3. int cal[N], sa[N], rak[N], height[N];
    4. int cmp(int *r,int a,int b,int l)
    5. {return r[a]==r[b]&&r[a+l]==r[b+l];}
    6. void get_sa(int *r,int *sa,int n,int M) {
    7. int i,j,p,*x=wa,*y=wb,*t;
    8. for(i=0;i0;
    9. for(i=0;i
    10. for(i=1;i-1];
    11. for(i=n-1;i>=0;i--) sa[--wss[x[i]]]=i;
    12. for(j=1,p=1;p2,M=p) {
    13. for(p=0,i=n-j;i
    14. for(i=0;iif(sa[i]>=j) y[p++]=sa[i]-j;
    15. for(i=0;i
    16. for(i=0;i0;
    17. for(i=0;i
    18. for(i=1;i-1];
    19. for(i=n-1;i>=0;i--) sa[--wss[wv[i]]]=y[i];
    20. for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i
    21. x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
    22. }
    23. return;
    24. }
    25. void get_height(int *r,int *sa,int n) {
    26. int i,j,k=0;
    27. for(i=1;i<=n;i++) rak[sa[i]]=i;
    28. for(i=0;i
    29. for(k?k--:0,j=sa[rak[i]-1];r[i+k]==r[j+k];k++);
    30. for(int i=n;i;i--)rak[i]=rak[i-1],sa[i]++;
    31. }
    32. // 调用方法举例
    33. while(~scanf("%s",s + 1)){
    34. n = strlen(s + 1);
    35. for (int i = 1; i <= n; i++) {
    36. cal[i] = s[i];
    37. } // 将s[i]数组转换为cal数组,如果全是小写字符,可以将字符集的范围缩小到26
    38. cal[n + 1] = 0;
    39. get_sa(cal + 1, sa, n + 1, 300); // 300 是ASCII码的范围
    40. get_height(cal + 1, sa, n);
    41. }

    题目链接:Problem - 1403 (hdu) Longest Common Substring

    题目描述:

            求两个字符串S、P的最长公共子串的长度,(最长公共字符串也可以求出

    题目分析:

            height数组表示相邻两个后缀字符串的最大前缀匹配数,所以,求最大的height即可。后缀数组求法,针对的是一个字符串,所以我们采用将两个字符串通过‘#’连接起来。注意在取最大的height时,相邻的两个后缀字符串必须一个是属于S的,一个是属于P的。

    1. // problem :
    2. #include
    3. using namespace std;
    4. #define ll long long
    5. typedef pair<int, int> PII;
    6. #define pb push_back
    7. const int N = 200005;
    8. int wa[N],wb[N],wv[N],wss[N];
    9. int cal[N], sa[N], rak[N], height[N];
    10. int cmp(int *r,int a,int b,int l)
    11. {return r[a]==r[b]&&r[a+l]==r[b+l];}
    12. void da(int *r,int *sa,int n,int M) {
    13. int i,j,p,*x=wa,*y=wb,*t;
    14. for(i=0;i0;
    15. for(i=0;i
    16. for(i=1;i-1];
    17. for(i=n-1;i>=0;i--) sa[--wss[x[i]]]=i;
    18. for(j=1,p=1;p2,M=p) {
    19. for(p=0,i=n-j;i
    20. for(i=0;iif(sa[i]>=j) y[p++]=sa[i]-j;
    21. for(i=0;i
    22. for(i=0;i0;
    23. for(i=0;i
    24. for(i=1;i-1];
    25. for(i=n-1;i>=0;i--) sa[--wss[wv[i]]]=y[i];
    26. for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i
    27. x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
    28. }
    29. return;
    30. }
    31. void calheight(int *r,int *sa,int n) {
    32. int i,j,k=0;
    33. for(i=1;i<=n;i++) rak[sa[i]]=i;
    34. for(i=0;i
    35. for(k?k--:0,j=sa[rak[i]-1];r[i+k]==r[j+k];k++);
    36. for(int i=n;i;i--)rak[i]=rak[i-1],sa[i]++;
    37. }
    38. char s[N];
    39. int n1, n;
    40. int main(){
    41. while(~scanf("%s",s + 1)){
    42. n1 = strlen(s + 1);
    43. s[n1 + 1] = '$';
    44. scanf("%s", s + n1 + 2);
    45. n = strlen(s + 1);
    46. for (int i = 1; i <= n; i++) {
    47. cal[i] = s[i];
    48. }
    49. cal[n + 1] = 0;
    50. da(cal + 1, sa, n + 1, 300); // 300 是ASCII码的范围
    51. calheight(cal + 1, sa, n);
    52. int ans = 0;
    53. int pos = -1;
    54. for (int i = 2; i <= n; ++i) {
    55. if (height[i] > ans &&
    56. ((sa[i] < n1 && sa[i - 1] >= n1) || (sa[i - 1] < n1 && sa[i] >= n1)))
    57. ans = height[i], pos = sa[i];
    58. }
    59. printf("%d\n", ans);
    60. // 最长公共字符串
    61. for (int i = 1; i <= ans; ++i) {
    62. printf("%c", s[pos + i - 1]);
    63. }
    64. puts("");
    65. }
    66. return 0;
    67. }
  • 相关阅读:
    leetcode经典例题——单词拆分
    【英语:基础进阶_听口实战运用】D4.听力题目的类别与应对方法
    Composer初次接触
    重生之我是一名程序员 38
    欧姆电阻测量原理
    面试—如何介绍项目中的多级缓存?
    【10】使用Test类测试&ImGUI
    通过热敏电阻计算温度(三)---Marlin实现分析
    MOOS程序解析记录(7)pMarinePID解析
    数据结构——图
  • 原文地址:https://blog.csdn.net/PURSUE__LIFE/article/details/126509974