• KMP&拓展KMP 复习笔记


    KMP

    引入

    思考一个经典问题:求模式串 T T T在文本串 S S S中第一次出现的位置。


    考虑朴素做法:枚举 S S S的每一位考虑将其设为 T T T开头所处的位置,依次判断,复杂度 O ( ∣ S ∣ × ∣ T ∣ ) O(|S|\times |T|) O(S×T)
    换句话说,若匹配,比较下位;否则,回到上次的下一个位置位置继续匹配,即 S i ≠ T j S_i \neq T_j Si=Tj时, i → i − j + 1 , j → 0 i\rightarrow i-j+1,j\rightarrow 0 iij+1,j0
    能否减少跳转,使 i i i线性, j j j尽量少跳转,达到 O ( n + m ) O(n+m) O(n+m)的时间复杂度?请看下文KMP算法。

    算法

    考虑失配情况。虽然当前为不匹配,但之前位是完全匹配的,即 S i − j . . . i − 1 = T 0... j − 1 S_{i-j...i-1}=T_{0...j-1} Sij...i1=T0...j1,这也正是我们所遗漏的条件。
    令此时 j j j跳转到 k k k继续和 S i S_i Si匹配,即:
    在这里插入图片描述

    图中的两次等于列成式子就是:
    { S i − j . . . i − 1 = T 0... j − 1 S i − k . . . i − 1 = T 0... k − 1

    {Sij...i1=T0...j1Sik...i1=T0...k1" role="presentation" style="position: relative;">{Sij...i1=T0...j1Sik...i1=T0...k1
    {Sij...i1=T0...j1Sik...i1=T0...k1
    这个式子是关于两个字符串的,有些复杂,可否将其化成仅关于 T T T的等式,即消掉等式左边的 S S S?由第一个等式进一步推得 S i − k . . . i − 1 = T j − k . . . j − 1 S_{i-k...i-1}=T_{j-k...j-1} Sik...i1=Tjk...j1,将第二个式子带入,得 T 0... k − 1 = T j − k . . . j − 1 T_{0...k-1}=T_{j-k...j-1} T0...k1=Tjk...j1

    也就是 T T T的前缀和后缀相同,因为我们要尽量少往回跳,所以 k k k要尽量大;而且,这个前缀和后缀不能是其本身,所以 k < j kk<j此时符合条件的 k k k即为 f a i l j fail_j failj,没有即为-1

    举个栗子,求字符串"qwqwqawawa"的 f a i l fail fail

    字符fail
    q-1
    w-1
    q0
    w1
    q2
    a-1
    w-1
    a-1
    w-1
    a-1

    如何求出 f a i l fail fail数组?不着急,先看如何利用求好的 f a i l fail fail匹配。

    int KMP(char *s, char *t) {
    	int n = strlen(s), m = strlen(t), j = -1;
    	for (int i = 0; i < n; ++i) {
    		while (~j && t[j + 1] != s[i]) {	// 失配:跳转
    			j = fail[j];
    		}
    		if (t[j + 1] == s[i]) {	// 匹配:下一位
    			++j;
    			if (j + 1 == m) {	// 第一次匹配完所有字符:返回
    				return i - m + 1;
    			}
    		}
    	}
    	return -1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    我们对着这个代码想,就可以得出,求解 f a i l fail fail的过程即是 T T T自身匹配的过程,用前面的 f a i l fail fail算出后面的 f a i l fail fail,代码类似。

    void get_fail(char *s) {
    	int n = strlen(s);
    	fail[0] = -1;
    	for (int i = 1; i < n; ++i) {
    		int j = fail[i - 1];
    		while (~j && s[j + 1] != s[i]) {
    			j = fail[j];
    		}
    		if (s[j + 1] == s[i]) {
    			++j;
    		}
    		fail[i] = j;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    习题

    关于kmp的习题,不仅是关于其主要求解问题——字符串匹配,有部分利用了其 f a i l fail fail数组。

    POJ 2406 Power Strings

    这里就充分利用的 f a i l fail fail数组的特性, f a i l i fail_i faili为前 i i i个字符的最短公共前后缀的长度,既是最短,又是公共,不就是要求的循环节吗?这里我们需要判断 n n n是否可以被 f a i l n fail_n failn整除:若整除,答案为 n f a i l n \frac{n}{fail_n}% failnn;否则,答案为 1 1 1 s s s本身)。

    POI2006 OKR-Periods of Words

    这道题需要对 f a i l fail fail做二次处理,即长度减去重叠部分长度(长度-最小周期=最大周期),最后再将长度减去处理后 f a i l fail fail求和即可。

    CF1137B Camp Schedule

    我们可以从 s s s中得到 0 0 0 1 1 1的数量,则考虑填充 t t t。注意因为前缀后缀可能相同,所以不需要填充整个 t t t,而公共前后缀正好匹配 f a i l fail fail的性质,则每次填充完跳转到 f a i l fail fail处即可。

    拓展KMP

    这里应用没有kmp算法广泛,本文中适当略写。

    问题

    S S S所有后缀与 T T T的最长公共前缀(lcp),要求时间复杂度 O ( ∣ S ∣ + ∣ T ∣ ) O(|S|+|T|) O(S+T)

    算法

    思想和kmp有些类似,将其 f a i l fail fail数组改为 n e x t next next,含义由字符串前缀和此位置结束子串相等转变为和此位置开始子串相等,还是基于 T T T构造

    next

    还是递推。设 n e x t 0... x − 1 next_{0...x-1} next0...x1均已求得,求 n e x t x next_x nextx
    定义 p p p为之前所有位置能匹配到的最远位置,即 m a x { i + n e x t i − 1 } max\{i + next_i - 1\} max{i+nexti1},令 k k k为此时的 i i i,则根据 n e x t next next的定义,红色部分相等:
    在这里插入图片描述
    从两部分各取长度 p − x + 1 p-x+1 px+1后缀,则绿色部分相等:
    在这里插入图片描述
    y = n e x t x − k y=next_{x-k} y=nextxk,则与红色部分相同,蓝色部分相等:

    在这里插入图片描述
    又因红色部分相等,三处蓝色部分相等:
    在这里插入图片描述
    所以,如果一个蓝色的长度比一个绿色的长度小,也就是说后面位置的lcp不能比前面的还大或者等于,形式化的, y < p − x + 1 → x + y ≤ p yy<px+1x+yp那么 n e x t x = y next_x=y nextx=y;否则,两个指针分别从 y + 1 y+1 y+1 p + 1 p+1 p+1开始枚举。

    void get_next(char *s) {
    	int n = strlen(s), p = 0, k = 1;
    	next[0] = n;
    	while (p < n - 1 && s[p] == s[p + 1]) {
    		++p;
    	}
    	next[1] = p;	// next[0 & 1]: 手动计算
    	for (int i = 2; i < n; ++i) {
    		p = k + next[k] - 1;
    		int y = next[i - k];
    		if (i + y <= p) {	// 分类讨论
    			next[i] = y;
    		} else {
    			int j = max(p - i + 1, 0);	// 注意特判
    			while (i + j < n && s[i + j] == s[j]) {
    				++j;
    			}
    			next[i] = j, k = i;	// k直接更新,最新匹配一定比之前的p大
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    ex

    刚才说的这么多,有童鞋会问,整个 n e x t next next都是基于 T T T后缀与 T T T匹配的,如何用来进行 S S S后缀与 T T T的匹配呢?
    我们定义 e x i ex_i exi S S S的后缀 i i i T T T的lcp,还是设 e x 0... x − 1 ex_{0...x-1} ex0...x1均已求出,求 e x x ex_x exx
    同理,已知 S k . . . p = T 1... e x k S_{k...p}=T_{1...ex_k} Sk...p=T1...exk p = m a x { i + n e x t i − 1 } p=max\{i + next_i - 1\} p=max{i+nexti1}
    我们可以先利用 n e x t k next_k nextk求出红色部分,再利用 n e x t x − k next_{x-k} nextxk求出蓝色部分,其余过程一样。

    void get_ex(char *s, char *t) {
    	int n = strlen(t), m = strlen(s), p = 0, k = 0;
    	while (p < min(n, m) && s[p] == t[p]) {
    		p = 0;
    	}
    	ex[0] = p;
    	for (int i = 1; i < m; ++i) {
    		p = k + ex[k] - 1;
    		int y = next[i - k];
    		if (i + y <= p) {
    			ex[i] = y;
    		} else {
    			int j = max(p - i + 1, 0);
    			while (i + j < m && j < n && s[i + j] == t[j]) {
    				++j;
    			}
    			ex[i] = j, k = i;
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    今天复习内容就到这里,感谢收看~

  • 相关阅读:
    轻松搭建Linux的环境
    [附源码]Python计算机毕业设计Django校园一卡通服务平台
    【科技素养】蓝桥杯STEMA 科技素养组模拟练习试卷E
    [React] 性能优化相关 (二)
    java计算机毕业设计HTML5互动游戏新闻网站设计与实现源码+数据库+系统+lw文档
    JS高级(三):严格模式、闭包、递归、深拷贝和浅拷贝
    社交媒体搜索引擎优化及其重要性
    vue实现文件上传压缩优化处理
    vue2、vue3、react响应式原理、组件声明周期阐述与对比
    采用php vue2 开发的一套医院安全(不良)事件管理系统源码(可自动生成鱼骨图)
  • 原文地址:https://blog.csdn.net/yueyuedog/article/details/126215296