字典树+KMP+AC自动机

简介:                                                  1:字典树,又称单词查找树,Trie树,是一种树形结构,哈希表的一个变种。

                                                 <<字典树模板>>

1:字典树,又称单词查找树,Trie树,是一种树形结构,哈希表的一个变种。用于统计,排序和保存大量的字符串(也可以保存其他的)。优点就是利用公共的前缀来节约存储空间。在这举个简单的例子:比如说我们想储存3个单词,sky、skyline、skymoon。如果只是单纯的按照以前的字符数组存储的思路来存储的话,那么我们需要定义三个字符串数组。但是如果我们用字典树的话,只需要定义一个树就可以了。在这里我们就可以看到字典树的优势了。

2 在trie树中,每一个单词对应不同的一条路径只有当单词完全相同的时候才有可能出现同一个节点有多个单词结尾的情况,所以如果说所有的单词都不同那么以某个节点为结尾的单词数肯定为1。(画图)

3 注意如果是多组的数据一般采用静态分配的方法

动态分配

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;

struct Trie{
   int count;/*这个根据需要变化*/
   Trie *child[26];
   /*初始化节点*/
   Trie(){
      for(int i = 0 ; i < 26 ; i++)
         child[i] = NULL;
      count = 0;
   }
};
Trie *root;

/*字典树的插入*/
void insert(char *str){
    Trie *s = root;
    for(int i = 0 ; i < strlen(str) ; i++){
       int num = str[i]-'a';
       /*如果以str[i]为首的节点为空*/
       if(s->child[num] == NULL)
         s->child[num] = new Trie();/*创建新的节点*/
       s = s->child[num];
       (s->count)++;/*以s之前为前缀的字符串的个数*/
    }
}

/*字典树的查找*/
int search(char *str){
    Trie *s = root;
    int tmp_count = 0;
    for(int i = 0 ; i < strlen(str) ; i++){
       int num = str[i]-'a';
       /*如果以str[i]为前缀的节点为空,直接返回0*/
       if(s->child[num] == NULL)
         return 0;
       else{
         s = s->child[num];
         tmp_count = s->count;
       }
    }
    return tmp_count;
}

int main(){
   root = new Trie();


   return 0;
}
 


静态分配

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;

#define MAXN 100000/*最多的节点个数*/
#define N 30

int cnt;
struct Trie{
   int count;/*这个根据需要变化*/
   Trie *child[N];
   /*初始化节点*/
}trie[MAXN];
Trie *root;

/*静态分配空间*/
Trie* newTrie(){
     trie[cnt].count = 0;
     for(int i = 0 ; i < N;  i++)
         trie[cnt].child[i] = NULL;
     return &trie[cnt++];
}

/*字典树的插入*/
void insert(char *str){
    Trie *s = root;
    for(int i = 0 ; i < strlen(str) ; i++){
       int num = str[i]-'a';
       if(s->child[num] == NULL)
         s->child[num] = newTrie();/*创建新的节点*/
       s = s->child[num];
    }
}

/*字典树的查找*/
bool  search(char *str){
    Trie *s = root;
    int tmp_count = 0;
    for(int i = 0 ; i < strlen(str) ; i++){
       int num = str[i]-'a';
       /*如果以str[i]为前缀的节点为空,直接返回0*/
       if(s->child[num] == NULL)
         return false;
       s = s->child[num];
    }
    return true;
}

int main(){
   cnt = 0;/*初始化节点数为0*/
   root = newTrie();/*给根节点分配空间*/
   return 0;
}
 



                                                                                                                KMP算法
1 kmp是用来处理字符串的模式匹配问题,只能够匹配单一的字符串。文本串是不回溯的,模式串回到下一个匹配位置。
2 kmp的算法的过程:
  1:假设文本串的长度为n,模式串的长度为m;
  2:先例用O(m)的时间去预处理next数组,next数组的意思指的是当前的字符串匹配失败后要转到的下一个状

  态,next数组和text是无关的,所以可以事先预处理;
  3:利用o(n)的时间去完成匹配;
3 时间复杂度为o(n+m)即o(n);

4 kmp很适合处理的一类问题就是给定一个串B和一序列的串A,问B是哪些A的子串。

KMP中我们用两个指针文本串 i 模板串 j分别表示。A[i-j+1..i]B[1..j]完全相等。

  也就是说,i 是不断增加的(文本串是不回溯的),随着i 的增加j 相应地变化,且j 满足以A[i]结尾的长度为j的字符串正好匹配B串的前j个字符,当A[i+1] !=   B[j+1]KMP的策略是调整j的位置(j = next[j])使得A[i-j+1..i]B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配(从而使得ij能继续增加)

6 next的介绍(重点):

   1:next数组只和模式串本身有关和文本串是无关的,因为next表示的是当匹配失败后模式串要回溯到哪个位置。
   2:next数组存储的数据是用来当模式串与主串不匹配的时候要模式串回退到第几个字符与主串再重新匹配,我们知道KMP算法的主串是不回朔的,当不匹配的时候我们不是退回到开始位置重新匹配,而是利用已经匹配的结果将模式串回朔到下一个位置,这个位置比开始位置更近一步;
简单的说就是next[ j ]的值保存的是当模式串中第 j 个字符与主串第 i 个字符不匹配时候,模式串中的哪个字符 重新与主串第 i 个再匹配,这样总的字符比较次数比从开始位置比较的次数就少了。

   3:next[j]存储的就是模式串前j-1个字符(包含j-1)里前缀和后缀最大的匹配长度;也就是有j = next[j] ; 假设有模式串“abcabx”,那么next[6] = 2,那么就是说next[len] = ans , 整个串的前缀和后缀最长匹配的长度就是ans

   4:在模式串与标准串进行匹配时,指向他们的指针分别为j、i;当p[j]!=s[i]时,j直接变为next[j],新的p[j]继续与s[i]比较,如果不相等继续进行此操作……那么数组next[j]同样反映了,在模式串p的第j个位置之前,p[0]~p[next[j]-1]与p[i-next[j]]~p[i-1]这两段是完全一样的。假设模式串为“abcdabx”,手动模拟即可知道。

   5:求最大的匹配数的时候应该注意匹配数的范围不能超过固定的范围。

   6:利用next数组求字符串的最小的循环节

         假设字符串的长度为len,那么最小的循环节就是cir = len-next[len] ; 如果有len%cir == 0,那么这个字符串就是已经是完美的字符串,不用添加任何字符;如果不是完美的那么需要添加的字符数就是cir - (len-(len/cir)*cir)),相当与需要在最后一个循环节上面添加几个。

  7  有关next的另外一个性质

      假设现在有一个字符串为ababxxxxabab。那么求出的next数组为00012001234,那么前缀和后缀最长的匹配数是4,然后下一个前缀和后缀匹配长度为next[4] = 2 , 然后下一个为next[2] = 0。
       所以有一个结论就是,假设当前求出的字符串的前缀和后缀的最长的匹配的长度为len,那么下一个满足的前缀和后缀互相匹配的长度为next[len]...依次

7 模板:

#define MAXN 1000010

int ans;
char text[MAXN];/*文本串*/
char pattern[MAXN];/*模式串*/
int next[MAXN];/*next数组*/

/*O(m)的时间求next数组*/
/*求next数组主要是利用自己匹配自己的思路*/
void getNext(){
    int m = strlen(pattern);
    next[0] = next[1] = 0;/*next的前面两个肯定都是0*/
    for(int i = 1 ; i < m ; i++){/*记住这里是从1开始*/
       int j = next[i];
       while(j && pattern[i] != pattern[j])/*匹配失败往失败方向滑动*/
          j = next[j];
       next[i+1] = pattern[i] == pattern[j] ? j+1 : 0;
    }
}

/*o(n)的时间进行匹配*/
void find(){
    int j = 0;/*初始化在模式串的第一个位置*/
    for(int i = 0 ; i < n ; i++){/*遍历整个文本串*/
       while(j && pattern[j] != text[i])/*顺着失配边走,直到可以匹配,最坏得到情况是j = 0*/
         j = next[j];/*相当与向右移动,那么最长的匹配数会减少的*/
       if(pattern[j] == text[i])/*如果匹配成功继续下一个位置*/
         j++;
       if(j == m){/*如果找到了直接输出*/
         ans++;/*ans是用来记录模式串在文本串中出现几次,求匹配的次数*/
         printf("%d\n" , i-m+2);/*输出在文本串中第一个匹配的位置,不是下标*/
         printf("%d\n" , i-m+1);/*输出在文本串中第一个匹配的位置,是下标*/
       }
    }
}


kmp的扩展

1 求解最长公共前缀问题

   extend[i]表示的是文本串text[i,n]和模式串pattern的最长前缀。next[i]则是利用自己匹配自己的原则求出,next[i]表示pattern[i , n]和patten的最长的公共前缀

模板:

int next[MAXN];
int extend[MAXN];
char text[MAXN];
char pattern[MAXN];

/*求extend模式串和文本串匹配*/
void getExtend(){
    int n = strlen(text);/*文本串的长度*/
    int m = strlen(pattern);/*模式串的长度*/
    int a = 0;
    int minLen = min(n , m);
  
    while(a < minLen && text[a] == pattern[a])
         a++;
    extend[0] = a;/*求出extend[0]的长度为a*/
    a = 0;/*重新赋值为0*/
    
    /*从1开始枚举*/
    for(int k = 1 ; k < n ; k++){
       int p = a+extend[a]-1;/*之前匹配到的最远的地方*/
       int l = next[k-a];/*上一次的匹配值*/
       if(k-1+l >= p){
         int j = max((p-k+1) , 0);
         while(k+j < n && j < m && text[k+j] == pattern[j])
             j++;
         extend[k] = j;
         a = k;
       } 
       else
         extend[k] = l;
    }
}

/*求next,就是自己匹配自己*/
void getNext(){
    int len = strlen(pattern);
    int a = 0;
    next[0] = len;/*第一个位置的LCP是len*/
    while(a < len-1 && pattern[a] == pattern[a+1])
        a++;
    next[1] = a;/*求出next[1]位置的LCP*/
    a = 1;/*a重新赋值为1*/
    
    /*从2开始枚举*/
    for(int k = 2 ; k < len ; k++){
       int p = a+next[a]-1;/*之前匹配过程中的最远的位置,-1是因为包括自己本身*/
       int l = next[k-a];/*求出上一次的next值*/
       if(k-1+l >= p){/*大于p则说明没有被探访过*/
         int j = max((p-k+1) , 0);
         while(k+j < len && pattern[k+j] == pattern[j])
              j++;
         next[k] = j;/*next[k] = j*/
         a = k;/*a赋值为k*/
       }
       else/*否则不用算直接为l*/
         next[k] = l;
    }
}



2 最小最大表示法(误区:不是对字符串进行排序然后得到就是最小或最大)

问题1:判断两个字符串s1和s2是否是循环同构“直接用s2去匹配s1+s1,看能否找到”

问题2:求一个字符串的最小或最大表示法

/*求最小表示*/
int getMin(){
   char tmp[MAXN];
   memcpy(tmp , words , sizeof(words));
   strcat(words , tmp);
   int len = strlen(words);
   int i = 0 , j = 1 , k = 0;
   while(i+k< len && j+k < len){
      if(words[i+k] == words[j+k])
        k++;
      else{
        if(words[i+k] > words[j+k])
           i = i+k+1;
        else
           j = j+k+1;
        k = 0;
        if(i == j)/*若滑动后i == j那么j++*/
           j++;
      }
   }
   return min(i , j);
}

/*求最大表示*/
int getMax(){
   char tmp[MAXN];
   memcpy(tmp , words , sizeof(words));
   strcat(words , tmp);
   int len = strlen(words);
   int i = 0 , j = 1 , k = 0;
   while(i+k< len && j+k < len){
      if(words[i+k] == words[j+k])
        k++;
      else{
        if(words[i+k] < words[j+k])/*就是这里改为小于即可变成求最大的表示法*/
           i = i+k+1;
        else
           j = j+k+1;
        k = 0;
        if(i == j)/*若滑动后i == j那么j++*/
           j++;
      }
   }
   return min(i , j);
}


AC自动机

1  AC自动机是处理多模式串匹配的字符串算法

2  AC自动机的三个步骤
1 利用文本串建立字典树

2 在字典树上面构造失配指针
   设这个节点上的字母为C,沿着他父亲的失败指针走,直到走到一个节点,他的儿子中也有字母为C的节点。然后把当前节点的失败指针指向那个字目也为C的儿子。如果一直走到了root都没找到,那就把失败指针指向root
最开始,我们把root加入队列(root的失败指针显然指向自己),这以后我们每处理一个点,就把它的所有儿子加入队列,直到搞完。

3 在字典树上面匹配。
  一开始,Trie中有一个指针t1指向root,待匹配串(也就是“文章”)中有一个指针t2指向串头。
  接下来的操作和KMP很相似:如果t2指向的字母,是Trie树中,t1指向的节点的儿子,那么t2+1 , t1改为那个儿子的编号,否则t1顺这当前节点的失败指针向上找,直到t2是t1的一个儿子,或者t1指向根。如果t1路过了一个绿色的点,那么以这个点结尾的单词就算出现过了。或者如果t1所在的点可以顺着失败指针走到一个绿色点,那么以那个绿点结尾的单词就算出现过了。如果找到了以后还要继续往失配边移动,从而找到所有的单词。

在trie树中,每一个单词对应不同的一条路径只有当单词完全相同的时候才有可能出现同一个节点有多个单词结尾的情况,所以如果说所有的单词都不同那么以某个节点为结尾的单词数肯定为1。(画图)

模板:(以hdu2222为例)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;

#define MAXN 1000010
#define N 26/*这个地方看是字母还是数字*/

struct Node{
    Node *next;/*失配指针*/
    Node *child[N];/*字典树最多的儿子节点的个数*/
    Node *parent/*有时候需要输出以当前节点为结束的单词*/
    int number;/*标记单词的编号*/
};
Node node[MAXN];
Node *root;
queue<Node*>q;
int cnt;
int vis[MAXN];

 /*字典树静态分配空间*/
Node* newNode(){
   node[cnt].next = NULL;
   node[cnt].number = 0;
   for(int i = 0 ; i < N ; i++)
      node[cnt].child[i] = NULL;
   return &node[cnt++];
}

/*字典树的插入*/
void insert(char *str , int x){
   Node *p = root;
   int len = strlen(len);/*放在循环外面计算*/
   for(int i = 0 ; i < len ; i++){
      int num = str[i]-'a';
      if(p->child[num] == NULL)
         p->child[num] = newNode();
      p = p->child[num];
   }
   p->number = x ;/*记录该节点为结尾的单词编号*/
}

/*求失配函数*/
void getNext(){
   while(!q.empty())
       q.pop();

   q.push(root);/*首先把根节点入队列*/
   root->next = NULL;/*根节点的nexe指向空(也可以自己)*/

   while(!q.empty()){
       Node *p = q.front();
       q.pop();
       for(int i = 0 ; i < N ; i++){/*层次遍历*/
          if(p->child[i] != NULL){
            q.push(p->child[i]);/*把所有的儿子节点全部压入队列*/
            
            Node *tmp = p->next;/*tmp是p的失配指针*/
            /*沿着失配边走直到某一个节点也有child[i]儿子就把当前p->child[i]的失配指针赋为tmp->child[i]*/
            while(tmp){
               if(tmp->child[i]){
                  p->child[i]->next = tmp->child[i];
                  break;
               }
               tmp = tmp->next;/*向失配边走*/
            }
            if(tmp == NULL)
              p->child[i]->next = root;/*到root还没找到则失配指针为root*/
          }
       }
   }
}

/*匹配的过程*/
int find(){
    int sum = 0;
    int len = strlen(text);
    
    Node *p = root;
    
    for(int i = 0 ; i < len ; i++){
       int num = text[i]-'a';
       while(p != root && p->child[num] == NULL)/*没有该儿子节点往失配方向移动*/
           p = p->next;
       if(p->child[num] != NULL){
          p = p->child[num];/*指向儿子节点*/
          
          Node *tmp = p;
          while(tmp != NULL){/*从儿子节点开始往失配边方向向上移动匹配*/
             if(tmp->num){/*有时候是记录以该节点为结尾的单词的个数*/
              /*
               sum += tmp->num;/*以hdu2222为例,加上匹配的单词个数*/
               tmp->num = 0;
              */
              /*
               vis[tmp->number]++;/*找字符串出现的次数*/
              */ 
             }
             tmp = tmp->next;/*当找到一个模板串之和继续向失配边移动看有没有其它的串*/
          }
       }
    }
    return sum;
}

int main(){
   scanf("%d" , &Case); 
   while(Case--){
      scanf("%d" , &n);
      cnt = 0;
      root = newNode();
      /*输入单词建立trie树*/
      for(int i = 0 ; i < n ; i++){
         scanf("%s" , words[i]);
         insert(words[i] , i+1);
      }
      scanf("%s" , text);
      getNext();
      printf("%d\n" , find());
   }
   return 0;
}



目录
相关文章
|
7月前
哈夫曼编码和字典树
哈夫曼编码和字典树
50 0
AC自动机
AC自动机
62 0
|
7月前
KMP算next数组(2023 _ 7 _ 23 )笔记
KMP算next数组(2023 _ 7 _ 23 )笔记
44 0
|
7月前
|
存储 C++
leetcode-208:实现 Trie (前缀树/字典树)
leetcode-208:实现 Trie (前缀树/字典树)
53 0
|
算法
看了这个你基本就会算kmp算法的next数组了
看了这个你基本就会算kmp算法的next数组了
|
算法
KMP算法详解
KMP算法详解
123 0
KMP算法详解
|
算法 C语言
对于KMP的next数组的新发现,好像我们并不用回溯
对于KMP的next数组的新发现,好像我们并不用回溯
45 0
|
存储 机器学习/深度学习 算法
|
存储 算法 Linux
秒懂算法 | 字典树
字典树是一种基础方法,请掌握字典树的静态数组存储方法,它在后缀树、回文树、AC自动机、后缀自动机中都要用到。
156 0