如果你和本蒟蒻一样一开始不理解KMP的pi(民间又称next)数组

KMP算法

前置知识

一个字符串的border指的是【既是它的前缀又是它的后缀的子串】。
一个字符串的真子串不等于其自身。
对于字符串 s ,其前缀函数定义为

\pi(i)=\begin{cases}0&i=0\\max\{k|k<i,s[0...k-1]=s[i-(k-1)...i]\}&0<i<n\end{cases}

说人话: \pi(i) 即字符串s的border长度最大值。

在字符串匹配问题中,我们称匹配的字符串为主串(通常更长),尝试匹配的字符串为模式串(通常更短)。

前缀函数的模板

string s;
cin>>s;
int n = s.size();
s = " " + s;//1-based index
vector<int> pi(n+1);
for(int i = 2;i <= n;i++)
{
	int len = pi[i - 1]; //len即除了当前扫到的末尾外最长的前缀长度
	while(len > 0 && s[len + 1] != s[i])//s[len + 1] != s[i]表示:最长前缀的下一位与当前扫到的末尾不等同
		len = pi[len];//把“最长前缀长度”缩短为次大值
	if(s[len + 1] == s[i])//最长前缀的下一位与当前扫到的末尾等同,即没有参与过while循环
		len++;//扩展1位成功了
	pi[i] = len;//记录
}

KMP的模板

vector<int> pi;
string text,pattern;//text是主串,pattern是模式串

//...省略输入

vector<int> result; //存储模式串在主串中找到的开头下标(0-based)

/*if(pattern.empty() || text.size() < pattern.size())//显然不合法的情形
{
	cout<<"Impossible";
	return 0;
}*/

string combined = pattern + "#" + text;//0-based

//...省略求出combined的pi值过程

int plen =  pattern.size(), clen = combined.size();
for(int i = plen + 1;i < clen;i++)//思考一下即知,索引plen + 1即指向combined中text的首个字符
	if(pi[i] == plen)
		result.push_back(i-2*plen);//*

//解释一下这个for循环:指针i在text部分右移,如果pi[i]等于plen,就说明此时的后缀就是pattern,即找到了一个完全一致的子串pattern。

注释*举例演示:

pattern = "ate"
text = "latex"

combined a t e # l a t e x
i 0 1 2 3 4 5 6 7 8
i=7时找到pattern:ate。i-2*plen即7 - 2 * 3=1,而字符串text的下标1处正好为字符a。

鸣谢

感谢 @叶子宁 教练提供的 \pi(i) 函数求解代码。这对我的理解帮助巨大。

5 个赞

注意:本文中两段代码使用的首元素索引方式不同(0/1-based),直接并用会有问题,敬请注意调整。特此公告。

纯净版1-based kmp 函数示例(手动调整两处即可)

vector<int> getres(string pattern,string text)
{
	vector<int> result; 
	string combined = pattern + "#" + text;
	vector<int> pi = getpi(combined);
	int plen =  pattern.size(), clen = combined.size();
	for(int i = plen + 1;i < clen;i++)
		if(pi[i+1] == plen)
			result.push_back(i-2*plen+1);
	return result;
}

膜拜大佬%%%

1 个赞

我比您蒻多了qwq

咋可能

我初赛永远差2分

@唐一霄 最好介绍一下前缀函数的代码是如何工作的qwq

1 个赞

tql我不会 KMP/ll

%%%