zoukankan      html  css  js  c++  java
  • 07 字典树 KMP 后缀数组(待补)

     字典树

    定义

      其本质就是用一颗树来记录大量的字符串,主要适用于长度较小但是个数较多的情况,是一种数据结构。其核心思想是在插入时充分利用已有的字符串,也就是合并不同字符串的公共前缀。字典树的主要功能就是查找,每次的查询只和字符串长度有关所以就是$O(1)$复杂度,又由于其结构可以用来求两个字符串的$lcp$。初始化操作的复杂度为$O(nl)$,$l$为字符串长度(网上没找到,但应该就是这个)。

    实现

      这里用的是基地里的王大佬的板子,代码中的$ch$数组就是树,第一维是树的层(我自己瞎叫的,区别于深度),第二维对应$26$个字母(当然可以扩充,只要改变一下宏定义里的$id$就可以了),$end$数组是记录第$i$个节点是否为一个字符串的末尾,先上代码:

     1 struct Trie{
     2     int tot,ch[maxn][30],end[maxn];
     3     void init(){//#define cl(x) memset(x,0,sizeof(x))
     4         cl(ch),cl(end),tot=1;   
     5     }
     6     int append(int pos,int c){//在pos节点上插入编号为c('a'为0,依次往后)的字符
     7         int &t=ch[pos][c];
     8         return t?t:t=++tot;
     9     }
    10     int insert(string s){//#define id(x) (x-'a')
    11         int pos=1,i;
    12         for(int i=0;i<s.size();i++)
    13             pos=append(pos,id(s[i]));
    14         end[pos]++;
    15         return pos;
    16     }
    17     bool find(string a){
    18         ll u,v;
    19         u=0;
    20         for(int i=0;i<a.size();i++){
    21             v=id(a[i]);
    22             if(!ch[u][v]) return 0;
    23             u=ch[u][v];
    24         }
    25         if(!end[u]) return 0;
    26         return 1;
    27     }
    28 }tree;

      前两个函数直接看的话不太清楚,这里给出一组输入输出结果就更清楚了:

     1 input:
     2 rfcytcu
     3 rfcfe
     4 
     5 output:
     6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
     7 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0
     8 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
     9 0 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    10 0 0 0 0 0 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 0
    11 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 6 0 0 0 0 0 0
    12 0 0 7 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    13 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0 0 0 0 0
    14 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    15 0 0 0 0 10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    16 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

      输出是$ch$数组,具体代码就不贴了。可以看到实际的$ch[0]$就是根节点所在的那一层。在查找的时候一开始将$pos$赋值为$1$,也就是从$ch[1]$开始查找,查找的位置就是字符串当前字符的编号,然后如果不为$0$就说明已经插入了该字符,那么就继续往下查找,查找的层数编号恰好就是这个非零的值。在查找的时候有三种情况:

      1. 当走到被查找字符的末尾时恰好停止,此时判断$end[i]$如果不为$0$就说明这里是一个已经插入的字符说明字符存在;

      2. 如果字符串已经结束但是还未到任何一个$end$非零的节点说明字符不存在;

      3. 如果树已经到了一个尽头(就是后面没有子节点了),看到第$14$行,此时这一整行为$0$,那么就说明字符串不存在,这得益于在插入时的处理。

     思考题

    1. 求出整个集合中所有的公共前缀。

      遍历整棵树(一开始脑抽了,以为遍历是指数的,因为要遍历的话要一层一层的循环,但其实由于总的个数是小于等于$n$的,所以其实复杂度是$O(26n)$的),画图知道其实就是总节点数减去叶子节点。用$dfs$每次如果有一行不为零就跳到那一行,如果这个下一行全为$0$就说明这是一个叶子;总节点数就是结构体里面的$tot$,然后就完了。

    2. 给出一个字符串,问有多少字符串含有该前缀。

      多开一个数组记录经过该节点的个数,然后改一下$find$函数,如果存在就返回经过该点的次数。

     KMP算法

    定义

    $KMP$算法是一种改进的字符串匹配算法,其复杂度为$O(mn)$,$m,n$分别为主串长度和用于匹配的串的长度。在求解的过程中用到了$next$数组,该数组的定义是:一个字符串的前缀$pref_i$的最长公共前后缀的长度,就是首尾部分相同的最长子串(不包括本身),又由于在匹配过程中为了更加方便,所以一般$next_i$对应的是从第$0$号元素到第$i-1$号元素的值。

     实现

      $KMP$算法本身很简单,就是在移动指针的时候有目的的移动,每次移动到尽可能多的匹配字符的位置。主要是如何得到$next$数组,在网上看了好久,总是感觉讲的较为繁琐(可能是我不集中的问题,当然可能我讲的也很繁琐)

      如果根据定义直接求解的话肯定是不够优秀的,所以需要进行优化,这里使用的是双指针。

      我们规定下标从$0$开始(网上也有从$1$开始,个人觉得从$0$开始便捷因为可以直接用字符串对象),这里使用两个指针(并不是那个指针只是代表一个位置)$j,k$。现在假设在某个位置如下所示:

      此时可以看到对应的$next_{j+1}$为$1$,接下来判断$s[j+1]$和$s[k+1]$,二者是相等的那么下一步就是:

       而此时的$next_{j+1}$就是上一步的$next_{j+1}$自加$1$。然后继续判断,发现这次不相等了:

      这里就相当于是原本大问题中的匹配失败,显然我们需要利用已有的$next$数组,这里其实就是返回$next_k$。这样就不全是直接回到初始位置,而是尽可能的判断是否有可以利用的信息,那么下一步就长这样:

      这次的例子返回了以后就直接到了第一个位置,那么再次判断发现二者不相同,就要重复上述步骤。

      重复当中,如果有相同的,那么就更新$next$的值;

      如果最终到达了初始位置(就是这张图的样子),这时就出现了一个问题:根据定义,$next_0$是不存在的(因为就只到$i-1$号元素,而$-1$号是不存在的),如果此时将这个变量初始化为$0$就会出现死循环;定义为一个正数,有可能会影响结果;所以干脆定义成$-1$(其实任何在整个过程中不会出现的数字都可以,$-1$最为常见),这样就只需要再加入一个特判就可以实现,以上就是大概流程,接下来看一看代码然后再自己写一下就可以体会到了:

     1 int nxt[maxn];
     2 void getnext(string s){//下标从零开始
     3     int j=0,k=-1,len=s.size();
     4     nxt[0]=-1;
     5     while(j<len){
     6         if(k==-1||s[k]==s[j]){
     7             nxt[++j]=++k;
     8         }else k=nxt[k];
     9     }
    10 }

      代码本身还是相当简单的,就$10$行。

     思考题

    1. 字符串匹配问题

    $KMP$算法的初衷,可以找到目标串在主串中出现的次数和位置。大概的做法就是,在原本暴力的基础上,每次移动的位数和$next$挂钩,这样最终的复杂度是$O(n+m)$。

    2. POJ 2406

    题意:给出一个串,找出其最小循环节的个数。

    分析:一开始想差了,以为是要求周期。利用$next$数组可知,如果一个串有循环节,那么循环节长度$=$总长度$-$next_n$。然后由于是循环节,那么一定有总长度可以整除循环节长度,然后就是裸题了。

    3. 牛客15071 数一数

    题意:给出若干串,定义$fleft( s,t ight) =$$s$在$t$中出现的次数,求出所有的$prod_{j=1}^n{fleft( s_i,s_j ight)}$。

    分析:脑筋急转弯,其实$f$很容易等于$0$,只要前者比后者长就莫得了,所以给所有串排个序,只有长度最小的那些可能不等于$0$;如果最小长度有两个或以上的不同字符串,那就全等于$0$了;如果只有一个,那就用$KMP$算法$O(n)$求出次数再累乘就可。

  • 相关阅读:
    蓝桥杯Java真题解析
    第十届蓝桥杯JavaB组总结
    Java框架相关
    认识JDBC
    Lua中 MinXmlHttpRequest如何发送post方式数据
    ios 的EditBox点击空白处不隐藏的解决方案
    AssetsManager 在ios更新失败解决方案
    Cocos Studio编辑器运行日志路径
    华为 进入和退出Fastboot、eRecovery和Recovery升级模式
    cocos lua 加密方案
  • 原文地址:https://www.cnblogs.com/Zabreture/p/13437799.html
Copyright © 2011-2022 走看看