• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
jacklee404
Never Stop!
博客园    首页    新随笔    联系   管理    订阅  订阅
kmp复习

KMP 复习

给定长度为\(n\)的字符串\(s\), 其前缀函数被定义为长度为\(n\)的数组\(\pi\)。其中\(\pi[i]\) 定义为:

  1. 若子串\(s[0...i]\)有一对相等的真前缀与真后缀: \(s[0...k-1]\)和\(s[i-(k-1)...i]\), 那么\(\pi[i]\)就是这个相等的真前缀(或者真后缀)的长度, 即\(\pi[i] = k\);
  2. 如果不止一对相等,则\(\pi[i]\), 取最大一对的长度。
  3. 若没有相等的, 则\(\pi[i] = 0\)

\[\pi[i] = \max\limits_{k=0...i}\left\{ k:s[0 \dots k-1]=s[i - (k-1) \dots i] \right \} \]

朴素做法

按照定义计算前缀函数的流程:

  • 在循环中以\(i = 1\rightarrow n - 1\)的顺序计算前缀函数\(\pi[i]\)的值(\(\pi[0] = 0\))。
  • 为了计算前缀函数值\(\pi[i]\),令变量\(j\)从最大的前缀长度\(i\)开始尝试。
  • 如果当前长度下真前缀和真后缀相等,则此时长度为\(\pi[i]\), 否则令\(j\) 自减少\(1\), 继续匹配,直到\(j = 0\).
  • 如果\(j = 0\)并且仍没有任何匹配,则置\(\pi[i] = 0\) 并移至下一个下标\(i + 1\)

复杂度\(O(n^3)\)

std::vector<int> prefix_fuc(std::string s) {
	int n = (int) s.length();
	std::vector<int> pi(n);
	for(int i = 1; i < n; i ++) {
		for(int j = i; j >= 0; j --) {
			if(s.substr(0, j) == s.substr(i - j + 1,j)) {
				pi[i] = j;
				break;
			}	
		}		
	}
	return pi;
}

优化一

​ 一个重要的观察是 相邻的前缀函数值之多增加1。

​ 参照如下,考虑:当取一个尽可能大的\(\pi[i + 1]\)时,必然要求新增的\(s[i + 1] = s[\pi[i]]\), 此时\(\pi[i + 1] = \pi[i] + 1\), 所以当移动到下个位置时,前缀函数值要么增加一,要么维持不变,要么减少。

\[\underbrace{\overbrace{s_0 \space s_1 \space s_2}^{\pi[i] = 3}\space s_3}_{\pi[i + 1] = 4} \cdots \underbrace{\overbrace{s_{i - 2} \space s_{i - 1} \space s_i}^{\pi[i]=3} \space s_{i + 1}}_{\pi[i + 1] = 4} $$ {12} ```C++ std::vector<int> prefix_fuc(std::string s) { int n = (int) s.length(); std::vector<int> pi(n); for(int i = 1; i < n; i ++) { for(int j = pi[i - 1] + 1; j >= 0; j --) { if(s.substr(0, j) == s.substr(i - j + 1, j)) { pi[i] = j; break; } } } return pi; } ``` ​ 因为存在$j = pi[i - 1] + 1$ ($pi[0] = 0$), 对于最大字符串比较次数的限制,可以看出每次只有在最好情况下才会为字符串比较次数的上限累积$1$, 而每次超过一次字符串比较消耗的是之后的增长空间。 ​ 由此我们可以得出字符串比较次数最多的一种情况: 至少$1$次字符串比较次数的消耗和最多$n - 2$次比较次数的积累,此时字符串比较次数为$n - 1 + n - 2 = 2n - 3$ 复杂度$O(n^2)$ ### 优化二 ​ 第一个优化中,我们讨论了计算$\pi[i + 1]$时最好的状况: $s[i + 1] = s[\pi [i]]$, 此时$\pi[i + 1] = \pi[i] + 1$。现在我们沿着这个思路走的更远一点: 讨论当$s[i + 1] \ne s[\pi[i]]$时如何跳转。 ​ 失配时,我们希望找到对于子串$s[0 \dots i]$,仅次与$\pi[i]$ 的第二长度$j$, 使得在位置$i$的前缀性质仍得以保持,也即$s[0 \dots j - 1] = s[i - j + 1 \dots i]$: \]

\overbrace {\underbrace{s_0\space s_1}j \space s_2\space s_3}^{\pi[i]}\space \dots \space \overbrace{s\space s_{i-2} \space \underbrace{s_{i - 1}\space s_i}j}^{\pi[i]}\space s

\[​ 如果找到长度$j$, 仅需要再次比较$s[j]$和$s[i+1]$。如果它们相等,那么就有$\pi[i + 1] = j + 1$。否则,我们需要找到子串$s[0\dots i]$仅次于$j$的第二长度$j^{(2)}$, 使得前缀性质得以保持,如此反复,直到$j = 0$。如果$s[i + 1] \ne s[0]$, 则$\pi[i + 1] = 0$。 ​ 观察上图可以发现,因为$s[0 \dots \pi[i] - 1] = s[i - \pi[i] + 1 \dots i]$, 所以对于$s[0\dots i]$的第二长度$j$, 有这样的性质: \]

s[0\dots j-1] = s[i - j + 1...j] = s[\pi[i] - j \dots \pi[i] - 1]

\[​ 也就是说$j$等价于子串$s[\pi[i] - 1]$的前缀函数值,即$j = \pi[\pi[i] - 1]$。同理,次于$j$的第二长度等价于$s[j - 1]$的前缀函数值,$j^{(2)} = \pi[j - 1]$ ​ 显然我们可以得到一个关于$j$的状态转移方程:$j^{(n)} = \pi[j^{(n - 1)} - 1] \space , (j^{(n-1)} > 0)$ ​ 最终我们可以构建一个不需要任何字符串比较,并且只进行$O(n)$次操作的算法。 ```C++ std::vector<int> prefix_fuc(std::string s) { int n = (int) s.length(); std::vector<int> pi(n); for(int i = 1; i < n; i ++) { int j = pi[i - 1]; while(j > 0 && s[i] != s[j]) j = pi[j - 1]; if(s[i] == s[j]) j ++; pi[i] = j; } return pi; } ``` \]

posted on 2023-02-27 21:35  Jack404  阅读(8)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3