随笔 - 97, 文章 - 22, 评论 - 81, 引用 - 0
数据加载中……

KMP

KMP

        KMP算法可以说所有数据结构书上都有,上大学的时候也陆陆续续学过三次,每次学完看似理解了,可是过了不到半年又忘记了,或许是因为代码太短,能写出来就以为自己会了,没有深入去理解,导致下次再来看的时候感觉很陌生,一定是这样的。

    今天看了matrix67对KMP的解释,很赞,附上地址:http://www.matrix67.com/blog/archives/115

    为了让老年的自己不用在迟暮的时候再学一遍KMP,还是决定把一些关键性的东西记录下来,如果那时候的自己看到自己当年写的这篇笔记能有恍然大悟的感觉,那么现在就不是在浪费时间了。

      定义:

          S[1... n]   目标串                           T[1...m]   模式串

      算法目的:

          从目标串S中找到一个子串和模式串T完全匹配。

      算法核心思想:

               1) 枚举i从1到n,在S[i-j...i-1]和T[1...j]完全匹配的前提下,判断S[i]是否和T[j+1]相等:

                        a) 如果相等,说明S[i-j...i]和T[1...j+1]完全匹配,那么i和j都自增1;

                        b) 如果不相等,则需要找到一个最大的j' < j,满足S[i-j'...i-1]和T[1...j']完全匹配;

               2) 当i=n或j=m的时候说明匹配结束,否则重复1);

      对于j'可以这样理解,由于前提是S[i-j...i-1]和T[1...j]完全匹配,如果要找到一个j'满足S[i-j'...i-1]和T[1...j']也完全匹配,那么T[1...j']必定为T[1...j]的后缀,证明如下:首先将以下的子串进行编号: A = S[i-j...i-1]     B = T[1...j]      C = S[i-j'...i-1]     D = T[1...j']

      因为A和B完全匹配,C和D完全匹配,由于C为A的后缀,所以D为B的后缀。

      当S[i]和T[j+1]不相等的时候需要调整j的值,调整完后的j = Next[j](这个Next[j]就是之前所说的j'),需要满足 T[1 ... Next[j] ] = = T[ j - Next[j] + 1... j ], 并且Next[j]的值最大,比较书面的说法就是Next[j]表示在模式串T中以第j个元素为结尾的最长后缀中满足它是T的前缀的后缀的长度

      举个例子,T = "ababaaba"的Next数组为 [0, 0, 1, 2, 3, 1, 2, 3]。

      由于Next数组表示的含义只和自身的性质有关,所以在没有目标串的情况下同样可以求出Next数组,KMP的精妙之处就在于求这个Next数组了。

      在上文中提到的S和T的匹配中,每次S[i-j...i-1]都是尽量找到最大的j使得它和T[1...j]完全匹配,当然有可能找不到这样的j,此时令j = 0,即 S[i,i-1]和T[1,0]匹配(这是两个空串,空串和空串也可以匹配,hohoho~~所以j是一定存在的)。如果现在把S换成T,那么问题就转化成了T[i-j...i-1]和T[1...j]的匹配问题了,如果T[i-j...i-1]和T[1...j]完全匹配,并且T[1...j]是和T[i-j...i-1]匹配的最长的串,那么 Next[i-1] 就是 j(思考一下红色字的定义就明白了),于是问题就转化成了T的自我匹配的过程了。
      算法复杂度:
            O(n+m)

    Next函数的求解非常简洁:

 1 #define MAXN 1000010
 2 int next[MAXN];
 3  
 4 // 传入的字符串下标需要以1开头
 5 void getNext(int m, char *str) {
 6 next[1] = 0;
 7 // 枚举模式串的每个位置,判断以当前字符结尾能够匹配到的最大前缀
 8 for(int j = 0, i = 2; i <= m; i++) {
 9     // 在str[i-j i-1]和str[1j] 完全匹配的前提下判断str[i]和str[j+1]是否相等
10     // 如果不相等,则减小j的值,直到匹配到完全相等位置
11         while( j > 0 && str[i] != str[j+1] ) j = next[j];
12         // 如果能够找到以i结尾的后缀和以j+1结尾的前缀完全匹配,j自增1。
13         if(str[i] == str[j+1]) j ++;
14         // 这里j有两种情况:
15         // j = 0    以i结尾的后缀找不到一个前缀和它完全匹配
16         // j > 0    以i结尾的后缀和以j结尾的前缀完全匹配,更新Next函数的值
17         next[i] = j;
18     } 
19 }

 

PKU 3461 Oulipo

题意:求一个匹配串T在目标串S中的出现次数。

题解:求出T的Next数组,然后和S进行KMP匹配,匹配时当j = =m的时候表示找到一个可行解,计数器+1,然后将Next[j]赋值给j,使得它的最长前缀能够继续和目标串进行匹配。

KMP匹配过程和Next数组的求解是一样的。

 

 1 // S[1n] 目标串
 2 // T[1m] 匹配串 
 3 int KMP(int n, char *S, int m, char *T) {
 4     int cnt = 0;
 5     for(int j = 0, i = 1; i <= n; i++) {
 6         while( j>0 && S[i] != T[j+1]) j = next[j];
 7         if(S[i] == T[j+1]) j++;
 8         if(j == m) {
 9             cnt ++;
10             j = next[j];
11         }
12     }
13     return cnt;
14 }

 

HDU 4763 Theme Section

题意:给定一个长度为N(1 <= N <= 106)的字符串S,问能否和模式串EAEBE进行匹配其中A和B表示任意随机字符,如果能匹配,输出E的最大可能长度,不能匹配输出0。

题解:首先利用KMP求出S的Next数组,那么S[1...Next[N]]、S[1...Next[Next[N]]]、S[1...Next[...[N]] ]必定能和S的后缀进行完全匹配,将这些Next[i]利用一次迭代求出来,最终的答案一定在这些值中,然后从大到小枚举这些值,判断可行性。

假设当前枚举长度为i,那么在S[i+1 ... N-i] 中如果能够找到一个长度为i的子串满足和S[1...i]完全匹配,那么i就是一个可行解,又因为枚举是从大到小进行的,所以i就是E可能的最大长度。

于是问题就转变成了判断S[i+1 ... N-i]中是否存在一个和S[1...i]完全匹配的子串。如果存在,那么必定存在一个k( 2*i <= k <= N-i ),使得S[k-i+1 ... k] = = S[1 ... i ],所以必定有Next[Next[...[k]]] = = i,所以我们可以预先将S[i+1 ... N-i]区间内所有的Next值退化后进行Hash,然后在枚举某个长度i的时候去Hash数组中找i是否被标记,如果被标记说明存在某个k满足S[k-i+1 ... k] = = S[1 ... i ],i就是最大可能长度。

 

HDU 2594 Simpsons’ Hidden Talents

题意:给定两个长度不大于50000的串,求两个串的一个最长公共子串满足子串为第一个串的前缀,并且为第二个串的后缀。

题解:将两个串用一个从未出现过的字符连接,拼成一个长度为N的串,然后进行一次自我匹配,求出next数组,根据Next数组的定义,Next[N]就是所求的最大长度。

 

HDU 3746 Cyclic Nacklace

题意:给定一个长度为N(N <= 105)的字符串S,求在它的末尾添加几个字符使得他变成一个至少重复两次的连续重复串,要求添加的字符数最少。

题解:首先利用KMP进行一次自我匹配求出Next数组,然后枚举重复串的长度i,令x = i * (N/i),如果x - Next[x] == i,说明S[x]是S的一个连续重复子串(或者叫连续重复前缀更加贴切),理由很简单,将字符串S[x]以长度i为单位分组,

S[1...i]   S[i+1...2i]  S[2i+1...3i]   ……   S[(N/i-1)i + 1...(N/i)i]

   S[1...i]   S[i+1...2i]   ……  S[(N/i-2)i + 1...(N/i-1)i]

由于x + i = = Next[x],可以列出连等式,有如下等价关系:S[1...i] = = S[i+1...2i] = = ... = = S[(N/i-1)i + 1...(N/i)i]。

那么剩下的就是要看S[x+1...N]是否为S的前缀,同样可以根据Next数组的定义进行判断,特殊的,当x == N时,S[x+1...N] == S[N+1,N]为空串,必定为S的前缀,也是满足条件的,枚举所有满足条件的长度L,取L - (N-x)的最小者就是答案了。

 

PKU 2406 Power Strings

题意:给定一个长度不超过N(N <= 106)的字符串,它一定是某个串重复K次得到,求这个K的最大值。

题解:假设子串T重复K次后得到串S,那么T的长度一定为L = N/K(要整除),则T = S[1...L],将S拆分成K份,每份长度为L,则有

S[1...L] = S[L+1...2L] = S[2L+1...3L] = ... = S[(K-1)L+1...KL]

由于要保证K最大,势必L要取最小,所以根据Next函数的定义,有Next[KL] = (K-1)L;

即Next[N] = N - L,所以L = N - Next[N];

但是得出的长度L还要保证能被N整除,所以如果不能整除说明L = N,即K = 1;而如果能整除,那么K = N / (N - Next[N]);

 

PKU 2752 Seek the Name, Seek the Fame

题意:给定一个长度为N(N <= 400000)的字符串,求它的前缀等于后缀的所有子串的长度。

题解:考察Next数组的定义。不断迭代求N的Next,Next[N]的Next......然后逆序输出即可。

 

 

 

HDU 3374 String Problem

题意:给定一个长度为N(N <= 106)的字符串S,然后将它进行左移,总共产生N个循环字符串,求其中字典序最小的串的编号以及这样的串的个数,和字典序最大的串的编号以及这样的串的个数。

题解:先求字典序最小的,字典序最大的只需要将每个字符用127减去本身再求一次字典序最小即可;定义两个指针i,j,i初始为0,j初始为1,再定义一个长度变量k = 0:

1) 比较S[i+k] 和S[j+k]的大小关系:

a) 如果相等,k自增1;当k==N则跳出循环,否则继续1)的比较;

b) 如果S[i+k] < S[j+k],j += k + 1, k = 0; 

c) 如果S[i+k] > S[j+k], i += k + 1, k = 0;

2) 如果i 和j相等,j自增1;当j==N或i==N则跳出循环,否则继续1)的比较;

 

这样循环结束后如果,取i和j的小者就是答案。

然后在利用求出来的下标,生成一个新的字符串作为匹配串和一个原串的两倍的串作为目标串进行KMP匹配,得到种数。

PKU 3690 Constellations

题意:给定N*M(N<=1000, M <= 1000)01矩阵S,再给定T(T <= 100)P*Q(P <= 50, Q <= 50)01矩阵,问P*Q的矩阵中有多少个是S的子矩阵。

题解:由于P <= 50,所以我们可以把所有P*Q的矩阵进行二进制位压缩,将P*Q的矩阵的每一列压缩成一个64位整数,这样P*Q的矩阵就变成了一个长度为Q的整数序列T,用同样的方式对N*M的矩阵进行压缩,总共可以产生(N-P+1)个长度为M的整数序列,剩下的就是进行最多(N-P+1)KMP匹配了。



posted on 2014-06-20 21:37 英雄哪里出来 阅读(3873) 评论(1)  编辑 收藏 引用 所属分类: 算法专辑

评论

# re: KMP  回复  更多评论   

学习了
2014-07-18 21:33 | xuezhanghao

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理