字符串

\(KMP\)

\(KMP\)是单模式匹配算法,在文本串中查找单个模式串。时间复杂度为\(O(m+n)\),空间复杂度为\(O(n)\)

模式匹配是指在长度为\(n\)的文本串\(S\)中查找某个长度\(m\)的模式串\(P\)

朴素的模式匹配算法

考虑暴力匹配,\(i\)指针扫描\(S\)\(j\)指针扫描\(P\)。每次从\(S\)\(i\)处开始,和\(P\)逐个比较,发现失配则\(i++\),重新与\(P\)匹配。暴力法的时间复杂度很不稳定,当\(S\)\(P\)差别很大时,往往在第一个位置就失配,复杂度近似\(O(n)\)。但是如果每次都匹配到前\(m-1\)个字符,才发现失配,那么复杂度最坏\(O(nm)\)

\(KMP\)算法思想

我们发现暴力法的缺点是,不管情况如何,只要失配那么\(i\)就会回溯到原位置再加一,这导致了很多多余的计算。\(KMP\)的优化的关键就是避免\(i\)的回溯,并使用了\(nxt\)数组。

我们考虑不同的失配情况:

\(1.\)

\(P\)在失配点之前的字符均不相同,此时失配,发现直接将\(P\)滑到最后失配处,即\(i\)不回溯,\(j=0\)从头匹配。

\(2.\)

\(P\)在失配点之前有字符相同,我们再细分为两种情况:

\((1)\)

相同的部分是\(P\)的前缀和后缀,那么同样当前在\(S\)匹配的一段,我们记为\(T\)\(T\)的前缀和后缀也相同,且\(S\)\(T\)的四段序列都相同。我们此时将\(P\)滑动到\(T\)的后缀,使\(P\)的前缀与\(T\)的后缀重合,再在此前缀后匹配。即\(i\)不回溯,\(j\)到前缀的后一个位置。

\((2)\)

相同的部分不是\(P\)的前缀和后缀,这意味着不可能再从前面中间开始匹配。即\(i\)不变,\(j\)回溯到\(0\)

现在我们只需要快速判断\(P\)当前匹配的部分\(T\)的前缀后缀是否相等了,这就需要\(nxt\)数组。

最长公共前后缀和\(nxt\)数组

\(nxt[j]\)表示前\(j\)个字符,前缀和后缀的最长交集的长度,把这个最长交集称为最长公共前后缀\((LPS\ Longest\ proper\ Prefix\ which\ is\ also\ Suffix)\)

可以\(O(m)\)递推出\(nxt\)数组,假设已经求出\(nxt[i]\),我们再向后匹配一个字符。前缀用\(j\)扫描,后缀用\(i\)扫描,我习惯字符串的下标从\(1\)开始,此时\(i+1,j=nxt[i]+1\)

如果\(p[i+1]=p[j]\),那么\(nxt[i+1]=nxt[i]+1\)

如果\(p[i+1]\neq p[j]\),那么我们要缩短交集的长度,令\(j'=nxt[j-1]+1\),这使得前后缀的交集长度缩短,再比较\(p[i+1]\)\(p[j']\),重复上述过程,直到\(p[i+1]=p[j']\),此时\(nxt[i+1]=nxt[j']+1\)

#include<bits/stdc++.h>
using namespace std;
char s1[1000005],s2[1000005];
int nxt[1000005];
int n,m,j;
int main(){
	scanf("%s",s1+1);
	scanf("%s",s2+1);
	n=strlen(s1+1);
	m=strlen(s2+1);
	for(int i=2,j=0;i<=m;i++){//求模式串的nxt数组
		j=nxt[i-1];
		while(j>0&&s2[j+1]!=s2[i]) j=nxt[j];//当前无法扩展,缩小交集范围
		if(s2[j+1]==s2[i]) nxt[i]=j+1;
	}
    int j=0;
	for(int i=1;i<=n;i++){
        while(j&&s1[i]!=s2[j+1]){//已经匹配了j位,现在匹配j+1位,如果失配就将j回溯
            j=nxt[j];
        }
        if(s1[i]==s2[j+1]) j++;//j+1位匹配成功
        if(j==m){
            cout<<i-m+1<<endl;
        }
	}
	for(int i=1;i<=m;i++){
		printf("%d ",nxt[i]);
	}
	printf("\n");
	return 0;
}

字典树

\(n\)个字符串中查找某个字符串,暴力匹配的复杂度为\(O(nm)\),但是考虑查字典的过程,我们查一个单词最多只要单词长度次数,字典树\(Trie\)就是模拟这个过程的数据结构。字典树是多叉树,英文的字典树是\(26\)叉的,数字的字典树是\(10\)叉的。字典树是很多其他算法和数据结构的基础,回文树,\(AC\)自动机,后缀自动机都建立在字典树上。

建立字典树

字典树一般可以如下建立:

struct trie{
 	<Type> data;
    bool isend; //是否是单词结尾
    trie *children[size]; //指向多个子节点,可能有很多空指针
}

字典树常见应用:

\((1)\) 字符串检索。可以检索、查询字符串。

\((2)\) 统计一个单词出现的次数

\((3)\) 字典序排序。插入时,在平级按字典序顺序插入,建好树后按先序遍历就得到字典序排序。

\((4)\) 前缀匹配。字典树一个节点后的节点都具有共同前缀,是按着公共前缀建树的,可以搜索相同前缀。

实现
posted @ 2025-04-07 20:10  青涩的Lemon  阅读(17)  评论(0)    收藏  举报