Z 函数(扩展 KMP)简介
约定:字符串下标以 0 为起点。
定义对于一个长度为 n 的字符串 s,定义函数 z[i] 表示 s 和 s[i,n-1](即以 s[i] 开头的后缀)的最长公共前缀(LCP)的长度,则 z 被称为 s 的 Z 函数。特别地,z[0] = 0。
国外一般将计算该数组的算法称为 Z Algorithm,而国内则称其为 扩展 KMP。
这篇文章介绍在 O(n) 时间复杂度内计算 Z 函数的算法以及其各种应用。
∀
\forall
∀(left,r), r = left+z[left]-1。如果 i
∈
\in
∈[left,r],则可以通过z[0…i-1]加速。
显然s[0…r-left] 等于 z[left,r]。
令i2 = i - left
∀
\forall
∀i1$in[0,right-i] s[i2+i1] = s[i+i1];
∀
\forall
∀i1$in[0,z[i2]-1] s[i1] = s[i2+i1];
两者联合:
iMin = min(z[i2]-1,right-i)
∀
\forall
∀i1$in[0,iMin ] s[i1] = s[i+i1]; 即:最长公共前缀至少是:s[0…iMin]余下部分,暴力统计。
我们不需要记录所有的(l,r),只需要记录r最大的(l,r)。
封装代码
class KMPEx
{
public:
template<class T>
static vector<int> ZFunction(const T* p, int n) {
vector<int> z(n);
z[0] = n;
for (int i = 1, left = 0, r = 0; i < n; ++i) {
if (i <= r) {//如果此if,r-i+1可能为负数
z[i] = min(z[i - left], r - i + 1);
}
while ((i + z[i] < n) && (p[z[i]] == p[i + z[i]])) {
z[i]++;
}
if (i + z[i] - 1 > r) left = i, r = i + z[i] - 1;
}
return z;//z[i] 表示S与其后缀S[i,n]的最长公共前缀(LCP)的长度
}
static vector<int> ZFunction(string s) {
return ZFunction(s.c_str(), s.length());
}
static int MinCyc(const string& str, int unit = 1) {
const int N = str.length();
auto z = ZFunction(str);
for (int k = unit; k < N; k += unit) {
if (z[k] >= N - k) { return k; }
}
return N;
}
};
单元测试
namespace UnitTestKMPEx
{
string s;
TEST_CLASS(UnitTest)
{
public:
TEST_METHOD(TestMethod0)
{
s = "babab";
auto v2 = KMPEx::ZFunction(s);
AssertEx(vector<int>{(int)s.length(), 0, 3, 0, 1}, v2);
}
TEST_METHOD(TestMethod2)
{
s = "azbazbzaz";
auto v2 = KMPEx::ZFunction(s);
AssertEx(vector<int>{(int)s.length(), 0, 0, 3, 0, 0, 0, 2, 0}, v2);
}
TEST_METHOD(TestMethod3)
{
s = "azbxddafgxxsdddffsssssgggbzaz";
auto v2 = KMPEx::ZFunction(s);
AssertEx(vector<int>{(int)s.length(), 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0}, v2);
}
TEST_METHOD(TestMethod4)
{
s = "azb1xdda3ddsfgazbxxsdddddddfazb1xfsaasssabsgggbzaz";
auto v2 = KMPEx::ZFunction(s);
AssertEx(vector<int>{(int)s.length(), 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0}, v2);
}
TEST_METHOD(TestMethod5)
{
s = "aaaaa";
auto v2 = KMPEx::ZFunction(s);
AssertEx(vector<int>{(int)s.length(), 4, 3, 2, 1}, v2);
}
};
}
时间复杂度
if (i <= r) {//如果此if,r-i+1可能为负数
z[i] = min(z[i - left], r - i + 1);
}
∀ \forall ∀i 时间复杂度是O(1),故总时间复杂度是O(n)
while ((i + z[i] < n) && (s[z[i]] == s[i + z[i]])) {
z[i]++;
}
如果相等,则一定是大于r,且r会相应增加,故相等的总时间复杂度是O(n)。
$\forall$i ,如果不等,只会执行一次,故不等的总时间复杂度也是O(n)
if (i + z[i] - 1 > r) left = i, r = i + z[i] - 1;
没有循环,显然时间复杂度是O(1),总时间复杂度是O(n)。
故总时间复杂度是O(n)。
寻找循环节
字符串s长度为n,求最大x ,s[0…m*x-1]由x个s[0…m-1]组成。如:s = ABABA,则x是2。
显然
:
z
[
m
]
>
=
(
x
−
1
)
×
m
:z[m] >= (x-1)\times m
:z[m]>=(x−1)×m。显然 s[m…]以x-1个s[0…m-1]开始,s[0…]以x个s[0…m-1]开始。
y
=
z
[
m
]
y = z[m]%M]
y=z[m] 则除x个完整循环节外,还有y个字符也是循环的。
题解
力扣:2223
故其和为: ∑ ( z ) \sum(z) ∑(z)
class Solution {
public:
long long sumScores(string s) {
vector<int> z = KMPEx::ZFunction(s);
long long llRet = 0;
for (const auto& n : z) {
llRet += n;
}
return llRet ;
}
};
扩展阅读
视频课程
有效学习:明确的目标 及时的反馈 拉伸区(难度合适),可以先学简单的课程,请移步优快云学院,听白银讲师(也就是鄙人)的讲解。
https://edu.youkuaiyun.com/course/detail/38771
如何你想快速形成战斗了,为老板分忧,请学习C#入职培训、C++入职培训等课程
https://edu.youkuaiyun.com/lecturer/6176
相关下载
想高屋建瓴的学习算法,请下载《喜缺全书算法册》doc版
https://download.youkuaiyun.com/download/he_zhidan/88348653
我想对大家说的话 |
---|
《喜缺全书算法册》以原理、正确性证明、总结为主。 |
闻缺陷则喜是一个美好的愿望,早发现问题,早修改问题,给老板节约钱。 |
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。 |
如果程序是一条龙,那算法就是他的是睛 |
测试环境
操作系统:win7 开发环境: VS2019 C++17
或者 操作系统:win10 开发环境: VS2022 C++17
如无特殊说明,本算法用**C++**实现。