• 【学习笔记】KMP 字符串匹配


    题外话

    最初学这个算法,是在新初一的暑假里,一个初三即将毕业的学长给我们讲的。当时听得不是很懂,模板题也就只有 28pts。

    后来过了好久,又想起来重新学一遍字符串,从 KMP 到 SAM 都来一遍。于是便有了这片文章。

    什么是 KMP 算法

    KMP 算法是由三名姓氏首字母分别为 K、M、P 的牛人发明的算法。虽然我不知道他们到底是谁hh

    就像标题中那样说的,KMP 是字符串的匹配算法。

    那什么叫做匹配呢?看下图就知道了:

    kmp-1.png

    接下来,我们会大量使用此类称呼,请务必记住这几个名称!

    注 1:若未特别说明,下面用 (n) 表示文本串 (S) 的长度,(m) 表示模式串 (T) 的长度。

    先来一个暴力

    众所周知,很多优秀的算法都由于暴力。KMP 也不例外。我们先从一个一般的暴力算法开始:

    1. 初始化,(i=1)(j=1)
    2. 开始匹配,如果 (S_i=T_j),则 (ileftarrow i+1)(jleftarrow j+1)
    3. 如果 (S_i eq T_j)(也称为失配),则重新令 (ileftarrow i-j+2)(jleftarrow 1)
    4. 特别地,如果 (j=m),即得到一次匹配,则令 (ileftarrow i-m+2)(jleftarrow 1)

    极其暴力,如果失配就从第一个没有匹配过的点开始。如果字符串是随机的可以跑得很优秀(因为很快就会失配),但是如果是精心构造过的数据就会被轻易卡到 (mathcal{O}(nm)) 从而爆炸。

    优化

    怎么给这种算法加上优化呢?

    我们注意到,之前的失配是非常浪费时间的。如果我们能够尽量利用失配前得到的结果,是不是就能在一定程度上提高程序效率,同时保证正确性呢?

    每一次匹配都为了找出可能存在的匹配方案,那么如果某些点可以在一开始就被证明是不可能存在匹配的,是不是就能节省很多时间呢?

    如下图:

    kmp-2.png

    在 KMP 算法中,这样的前缀/后缀被称为 border(有些也称为 next 数组)。这是整个 KMP 算法优化的关键。

    这样,我们就能完成匹配后的优化。那么怎么解决失配的优化问题呢?其实很简单。

    失配是什么?其实是模式串的一个前缀的匹配。所以我们也可以用匹配的优化来优化失配,只是指针的边界处理略有不同而已。

    经过了这样的优化,匹配的时间复杂度可以降到多少呢?注意到指向文本串的指针只增不减,而指向模式串的指针也是只增不减。每一次比较必然带来两个指针至少一个的变化,所以复杂度是 (mathcal{O}(n+m)),有了极大的优化。

    这一段的 Code:(我的写法的下标从 (0) 开始)

    ptr = -1;
    for (int i = 0; a[i]; i++) // 指向文本串的指针
    {
    	while (ptr != -1) // 失配时的下一步匹配
    	{
    		if (b[ptr+1] == a[i]) // 找到了下一个匹配
    			break;
    		ptr = fail[ptr]; // 这个可以暂时不用理解,接下来讲怎么计算 border 时会证明这一点
    	}
    
    	if (a[i] == b[ptr+1]) // 这么写而不是 (ptr == -1) 是因为最后一位也有可能匹配成功
    	{
    		ptr++; // 指针移动一位
    		if (ptr == lb - 1) // 发现一个匹配
    		{
    			printf("%d
    ", i - ptr + 1);
    			ptr = fail[ptr]; // 极大利用目前结果。
    		}
    	}
    }
    

    讲了这么多,我们似乎没有办法很快地求出 border,而且是模式串所有前缀的 border。我们需要引入 DP 思想。

    (f_i) 为到第 (i) 位的前缀的 border 长度。怎么转移呢?

    首先根据 border 的定义,如果 (T_{f_{i-1}+1}=T_i),也就是可以从上一位转移,即 (f_i=f_{i-1}+1)。但是如果失配了呢?

    这也是 KMP 极其精髓的一点,我们要证明一个定理,从而极大地优化复杂度:

    Theorem:一个 border 的 border 是原串的次大 border,即使字符串前缀和长度相等后缀一样的长度次大的方案。

    一张图看懂证明(字符串用线段表示):

    kmp-3.png

    这个性质至关重要,也就是说我们可以通过递归的方式来依次寻找最大、次大,从而转移。

    有了这个性质,之前那个用递归方式进行指针移动的思路也明白了。那么……复杂度呢?这个玄学的复杂度怎么分析呢?(我不会)总之,这里的复杂度也是线性的。

    贴代码时间到:

    fail[0] = -1, ptr = -1;
    for (lb = 1; b[lb]; lb++) // 遍历模式串
    {
    	while (ptr != -1) // 跳
    	{
    		if (b[ptr+1] == b[lb]) // 发现相同的
    			break;
    		ptr = fail[ptr]; // 继续跳
    	}
    	
    	if (b[ptr+1] == b[lb]) // 理由同上
    		ptr++;
    	fail[lb] = ptr; // 数组实际上存的是指针
    }
    

    完整代码

    KMP 算法的代码,可以通过模板题

    #include <cstdio>
    using namespace std;
    
    const int max_n = 1000000;
    char a[max_n], b[max_n];
    int fail[max_n+1];
    
    int main()
    {
    	int ptr, lb;
    
    	scanf("%s%s", a, b);
    
    	fail[0] = -1, ptr = -1;
    	for (lb = 1; b[lb]; lb++)
    	{
    		while (ptr != -1)
    		{
    			if (b[ptr+1] == b[lb])
    				break;
    			ptr = fail[ptr];
    		}
    		
    		if (b[ptr+1] == b[lb])
    			ptr++;
    		fail[lb] = ptr;
    	}
    
    	ptr = -1;
    	for (int i = 0; a[i]; i++)
    	{
    		while (ptr != -1)
    		{
    			if (b[ptr+1] == a[i])
    				break;
    			ptr = fail[ptr];
    		}
    
    		if (a[i] == b[ptr+1])
    		{
    			ptr++;
    
    			if (ptr == lb - 1)
    			{
    				printf("%d
    ", i - ptr + 1);
    				ptr = fail[ptr];
    			}
    		}
    	}
    
    	for (int i = 0; i < lb; i++)
    		printf("%d ", fail[i] + 1);
    	putchar('
    ');
    
    	return 0;
    }
    
  • 相关阅读:
    树洞留言板~
    异常The following attributes are mutually exclusive: asproute aspcontroller, aspaction asppage, asppagehandler
    持续交付:发布可靠软件的系统方法
    sql分页遍历出现重复数据原因与解决方案
    jenkens2权威指南
    timer
    window10、window11连接局域网共享打印机失败处理办法
    解决“Windows照片查看器无法显示此图片,因为计算机上的可用内存可能不足……”
    ts在项目中的使用三斜线引入 与 import区别
    vue unshift渲染遇到的坑
  • 原文地址:https://www.cnblogs.com/5ab-juruo/p/note-kmp.html
Copyright © 2020-2023  润新知