每天一颓: 均摊分析, pi函数和KMP算法

资料内容:
https://oi-wiki.org/string/kmp/


很久以前学过,写一些笔记作复习资料

一些概念:
真前缀, 真后缀等等不作介绍

(真前后缀匹配函数)前缀函数(pi函数):

\[\pi[i] = \max_{k = 0 \dots i}\{k: s[0 \dots k - 1] = s[i - (k - 1) \dots i]\} \]

特别规定,

\[\pi[0] = 0 \]


/*
Author: SJ
*/
#include<bits/stdc++.h>
const int N = 1e5 + 10;
using ll = long long;
using ull = unsigned long long;

std::string s1;
int pi[N];
void pi_query(std::string s1) {
	for (int i = 1; i < s1.size(); i++) {
		for (int j = i; j >= 0; j--) {
			if (s1.substr(0, j) == s1.substr(i - j + 1, j)) {
				pi[i] = j;
				break;
			}
		}
	}
}
int main() {
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);

	std::cin >> s1;
	pi_query(s1);
	for (int i = 0; i < s1.size(); i++) std::cout << pi[i] << ' ';
	return 0;
}

显然可以这样\(O(n^3)\)来算, 我们看看怎么加速

引理1: 相邻的pi-function最多增加1
证明: 类似LCS的证明, 反证法即可

代码即可优化成

void pi_query(std::string s1) {
	for (int i = 1; i < s1.size(); i++) {
		for (int j = pi[i - 1] + 1; j >= 0; j--) {
			if (s1.substr(0, j) == s1.substr(i - j + 1, j)) {
				pi[i] = j;
				break;
			}
		}
	}
}

我们介绍amortized analysis(主要是会计方法)说明一下这样计算是\(O(n^2)\)
首先if里面的substr函数是雷打不动的\(O(n)\)复杂度, 外面两层循环是\(O(n)\)的理由是这样: 两层循环不是independent的, 将前缀函数看作一个银行账户, 可以发现我们每天(每次循环)肯定固定花销是两块, 一次就配对成功, 则省了一块存银行里, 配对不成功, 会进行额外的花销, 但同时也会导致我们的"银行账户"下降, 也就是说可以看成把之前省的钱给花了, 那么摊下来就是最多\(2n - 2\)次花销, 也就是外面两层循环是\(O(n)\)


类似dp的思路, 我们可以往前跳转, 详情请看OI-Wiki,

那为什么这样子做直接就把复杂度变成惊人的\(O(n)\)了呢,我们还是觉得substr太慢了, 我们直接通过上面的性质把判真前后缀是否相等变成\(O(1)\), 最后就变成线性了

void pi_query(std::string s1) {
	for (int i = 1; i < s1.size(); i++) {
		int j = pi[i - 1];
		while (j > 0 && s1[i] != s1[j]) j = pi[j - 1];
		if (s1[i] == s1[j]) j++;
		pi[i] = j;
	}
}

kmp基本上就是前缀函数计算的一个直接应用

posted @ 2023-05-30 11:04  IHOPEIDIEYOUNG  阅读(98)  评论(0)    收藏  举报