算法初探 - 字符串匹配

更新记录

【1】2020.06.23-20:13

1.完善KMP内容
2.一点Trie树内容
3.AC自动机(弱化版)思想

现阶段内容并不是很完善,敬请期待下个版本

正文

KMP前言

ABOUT 暴力

如果你不会:
return 是新手?"别着急慢慢来,这篇博客可能不适合你":"暴力你再不会就趁早退役吧"

ABOUT KMP

KMP它就是普及的坎,过去就过去,过不去等死
其实没那么严重,前缀树也能打

翻找了很多关于KMP的博客,发现能让人真正理解这个算法的少之又少
所以我根据自己的理解来介绍一下这个算法,希望能帮到一些OIer

朴素算法

时间复杂度:\(O(nm)\)
实质就是暴力匹配,这里不过多介绍

KMP

为了方便讲述,我们先明确几个概念

  1. G位前(后)缀
    一个字符串的G位前(后)缀就是它前(后)G个字符组成的字符串
  • 例如字符串abgakdsf的3位前缀是abg,五位后缀是akdsf
  1. 最大相同前后缀
    满足以下两个条件:
  • G位前缀与G位后缀相同
  • 在满足上一条的情况下使得G最大
  • 例如字符串abcdbabc的最大相同前后缀是abc

先举个例子,我们要在abcaabcdabcaabcaabc中查找abcab

第一次匹配:
abcaabcdabcaabcaabc
||||X
abcab

普通的来看,这个字符不匹配就应该往后移动一位,然后继续进行匹配

但是这样完全将有用的信息忽略了

第一次匹配已经知道了前四个是abca
此时我们直接将字符串移动某个偏移量

abcaabcdabcaabcaabc
   |
   abcaa

然后继续进行匹配

某个偏移量到底是啥呢?

设主串M,模式T在进行匹配时
\(M[i-j]\;M[i-j+1]\cdots M[i]\)\(T[0]\cdots T[j]\)在进行匹配的时候
\(j-1\)位完全匹配,在第\(j\)位失配
如果
\(T[0]\;T[1]\cdots T[j-1]\ne T[1]\;T[2]\cdots T[j]\)
那么
\(T[1]\;T[2]\cdots T[j]\ne M[i-j]\;M[i-j+1]\cdots M[i]\)
所以当在某处失配的时候,我们去找这个字符串的最大相同前后缀

记录G的值,然后从G+1位继续匹配
这个G就是上文所说的某个偏移量
这个操作的正确性显然

在KMP算法中,我们定义一个next数组
第i位记录前i-1位最大G值
(当然也有用第i位记录前i位最大G值的,这里笔者习惯用前者)
那么如何去求next数组每一项的值呢?
我们用模式串自己匹配自己即可
即主串为T,模式串也为T

void getnext(){
	next[0]=-1;
	for(int i=0,j=-1;i<=s.length();){
		if(j==-1||s[i]==s[j])
			next[++i]=++j;
		else
			j=next[j];
	}
}

下面放一段运行过程

abcaabcdabcaabcaabc
i:0 j:-1
-1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
i:1 j:0
-1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
i:1 j:-1
-1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
i:2 j:0
-1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
i:2 j:-1
-1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
i:3 j:0
-1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
i:4 j:1
-1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
i:4 j:0
-1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0
i:5 j:1
-1 0 0 0 1 1 2 0 0 0 0 0 0 0 0 0 0 0 0
i:6 j:2
-1 0 0 0 1 1 2 3 0 0 0 0 0 0 0 0 0 0 0
i:7 j:3
-1 0 0 0 1 1 2 3 0 0 0 0 0 0 0 0 0 0 0
i:7 j:0
-1 0 0 0 1 1 2 3 0 0 0 0 0 0 0 0 0 0 0
i:7 j:-1
-1 0 0 0 1 1 2 3 0 0 0 0 0 0 0 0 0 0 0
i:8 j:0
-1 0 0 0 1 1 2 3 0 1 0 0 0 0 0 0 0 0 0
i:9 j:1
-1 0 0 0 1 1 2 3 0 1 2 0 0 0 0 0 0 0 0
i:10 j:2
-1 0 0 0 1 1 2 3 0 1 2 3 0 0 0 0 0 0 0
i:11 j:3
-1 0 0 0 1 1 2 3 0 1 2 3 4 0 0 0 0 0 0
i:12 j:4
-1 0 0 0 1 1 2 3 0 1 2 3 4 5 0 0 0 0 0
i:13 j:5
-1 0 0 0 1 1 2 3 0 1 2 3 4 5 6 0 0 0 0
i:14 j:6
-1 0 0 0 1 1 2 3 0 1 2 3 4 5 6 7 0 0 0
i:15 j:7
-1 0 0 0 1 1 2 3 0 1 2 3 4 5 6 7 0 0 0
i:15 j:3
-1 0 0 0 1 1 2 3 0 1 2 3 4 5 6 7 4 0 0
i:16 j:4
-1 0 0 0 1 1 2 3 0 1 2 3 4 5 6 7 4 5 0
i:17 j:5
-1 0 0 0 1 1 2 3 0 1 2 3 4 5 6 7 4 5 6
i:18 j:6
-1 0 0 0 1 1 2 3 0 1 2 3 4 5 6 7 4 5 6

KMP主过程:

while(l<m.length()){
		if(r==-1||m[l]==s[r]) l++,r++;
		else r=next[r];
		if(r==s.length()){
			cout<<l-s.length()+1<<"\n";
			r=next[r];
		}
	}

(想必您已经精通字符串了,快去吊打字典树吧)

Trie树前言

就是字典树(前缀树)啊

实现

每个节点有N个儿子,一个颜色标记
N取决于字符的个数,比如我插入的单词中只有小写字母,N开26即可

G位前缀相同的字符串,共用一个长度为G的链,在G+1处产生分支

[ A example ]
那么插入两个单词inne,innd
询问单词inn是否在Trie树中

显然inn在树中,但是我们并没有插入这个单词
这时候就要靠颜色标记了
对于一个字符串,在其最后一个字符所在的节点上将颜色标记置为1,代表我插入这个单词了

我们回归刚才的例子,插入单词时e与d标为1,别的不管

再次查找inn,虽然能找到但是我们发现n所对应的节点颜色标记为0
说明没有这个单词

Trie树基本思想结束

AC自动机

没有前言!
我承认我还没打代码

在Trie树上查找单词时,用KMP思想进行加速

AC自动机(基本)思想结束

(您觉得笔者讲的太简单,自己是字符串专家不需要看,那就快去吊打NOI/CTSC/IOI吧)

猜你喜欢

转载自www.cnblogs.com/zythonc/p/13178110.html