Z函数(扩展KMP)

本文介绍了Z函数(最长公共前后缀长度)及其优化方法(Z-box),展示了如何使用Z-box求解Z函数,并将其应用到扩展KMP算法中,用于计算两个字符串的后缀最长公共前缀。通过实例和代码演示,讲解了如何高效地处理字符串匹配问题。
摘要由CSDN通过智能技术生成

注意:对于字符串 S S S, S [ i , j ] S[i,j] S[i,j]表示从下标为 i i i的位置到下标为 j j j的位置所构成的子串。

Z函数

定义:对于字符串 s s s z i z_i zi 定义为 ∣ LCP ⁡ ( s , s [ i : ] ) ∣ |\operatorname{LCP}(s, s[i:])| LCP(s,s[i:]),即从 i i i 开始的后缀与 s s s 的最长公共子串的长度

。即 s [ 0 , z [ i ] − 1 ] = s [ i , i + z [ i ] − 1 ] s[0,z[i]-1] = s[i, i + z[i]-1] s[0,z[i]1]=s[i,i+z[i]1]最长。

暴力构造法: O ( n 2 ) O(n^2) O(n2)

cin >> p;
int n = strlen (p);

for (int i = 0; i < n; i ++) 
	while (i + z[i] < n and p[z[i]] == p[z[i] + i]) z[i] ++;
    
for (int i = 0; i < n; i ++)
	cout << z[i] << " ";

优化方法:

需要明白,优化方法一定是利用已有的信息来求未知的信息。

Z − b o x Z-box Zbox:相当于一段区间 [ l , r ] [l,r] [l,r],并且这段区间是字符串的前缀,其中要求 r r r要尽可能的大, Z − b o x Z-box Zbox会随着 i i i进行移动,因此,在位置 i i i时, [ l , r ] [l,r] [l,r]必须包含 i i i

对于字符串 b a c b c b a c b a bacbcbacba bacbcbacba Z Z Z函数的值如下

bacbcbacba
i n d e x index index 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 5 5 5 6 6 6 7 7 7 8 8 8 9 9 9
Z − f u n c t i o n Z-function Zfunction 10 10 10 0 0 0 0 0 0 1 1 1 0 0 0 4 4 4 0 0 0 0 0 0 2 2 2 0 0 0

z [ 5 ] = 4 z[5] = 4 z[5]=4, 下标为 5 5 5 Z − b o x Z-box Zbox范围为 s [ 5 , 8 ] s[5,8] s[5,8]
z [ 6 ] = 0 z[6] = 0 z[6]=0, 下标为 6 6 6 Z − b o x Z-box Zbox范围为 s [ 5 , 8 ] s[5,8] s[5,8]
z [ 7 ] = 0 z[7] = 0 z[7]=0, 下标为 7 7 7 Z − b o x Z-box Zbox范围为 s [ 5 , 8 ] s[5,8] s[5,8]
z [ 8 ] = 2 z[8] = 2 z[8]=2, 下标为 8 8 8 Z − b o x Z-box Zbox范围为 s [ 8 , 9 ] s[8,9] s[8,9]


如何用 Z − b o x Z-box Zbox来求 Z Z Z函数呢?(假定字符串用 S S S表示)

假定已经求得了 Z [ i − 1 ] Z[i-1] Z[i1]的值,以及到 i − 1 i-1 i1位置的 Z − b o x Z-box Zbox(即 [ l , r ] [l,r] [l,r])现在要求出 Z [ i ] Z[i] Z[i]的值。

我们可以比较 i i i r r r的大小,分成两种情况

1、 i > r i > r i>r

此时,虽然我们知道 i − 1 i-1 i1 Z Z Z值和 [ l , r ] [l,r] [l,r]但是这些信息并不能推导出 Z [ i ] Z[i] Z[i]的值,因此我们暴力的进行匹配求 Z Z Z的值,由于 r r r的值增加了,同时要更新 Z − b o x Z-box Zbox

2、 i < r i < r i<r

我们知道 [ l , r ] [l,r] [l,r]和字符串的前缀是相等的, 将红色的区间移动到开头,匹配出对应位置如下图

由于上面的两段红色区间是完全相等的(由 Z − b o x Z-box Zbox的定义),那么 s [ i − 1 + 1 , r − l + 1 ] = s [ i , r ] s[i-1+1, r-l+1] = s[i,r] s[i1+1,rl+1]=s[i,r],如下图绿色区域

在计算 Z [ i ] Z[i] Z[i]之前,所有的 Z Z Z都被计算过了,因此我们可以利用绿色的这段相等部分,来优化。

此时需要分为两种情况讨论

  • Z [ i − l + 1 ] < r − l + 1 Z[i-l+1] < r-l+1 Z[il+1]<rl+1

此类情况,可以直接利用前面的匹配信息, Z [ i ] = Z [ i − l + 1 ] Z[i]=Z[i-l+1] Z[i]=Z[il+1] ,同时 Z − b o x Z-box Zbox不变。

  • Z [ i − l + 1 ] > = r − l + 1 Z[i-l+1] >= r-l+1 Z[il+1]>=rl+1

此时, s [ i , r ] s[i,r] s[i,r]的部分一定是与前缀相同的,只需要在 > r >r >r的部分进行暴力匹配即可。

同时由于 r r r的范围更大了,需要更新 Z − b o x Z-box Zbox [ i , i + z [ i ] − 1 ] [i,i+z[i]-1] [i,i+z[i]1]

思考:

Z − b o x Z-box Zbox中的 r r r取值为什么要尽可能靠右?

原因在于越靠右,上图中绿色区域的范围会越大,这样 z [ i − l + 1 ] < r − l + 1 z[i-l+1]<r-l+1 z[il+1]<rl+1的情况会更多,所以会更好的节省时间。

总体时间复杂度为: O ( n ) O(n) O(n),因为 Z − b o x Z-box Zbox右端点最多右移 n n n次。

参考程序如下:

void get_z (char *s, int n) {

    int l = 0, r = 0;
    z[1] = n;
    for (int i = 2; i <= n; i ++) {
        if (i > r) {
            while (s[i + z[i]] == s[z[i] + 1]) z[i] ++;
            l = i, r = i + z[i] - 1;
        } 
        else if (z[i - l + 1] < r - i + 1) z[i] = z[i - l + 1];
        else {
            z[i] = r - i + 1;
            while (s[i + z[i]] == s[z[i] + 1]) z[i] ++;
            l = i, r = i + z[i] - 1;
        }
    }
}


扩展KMP

扩展KMP算法主要解决以下字符串问题:

给定两个字符串 s t r 1 str1 str1以及 s t r 2 str2 str2,求 s t r 1 str1 str1的每一个后缀与 s t r 2 str2 str2 L C P LCP LCP(最长公共前缀)。

类比:

相比于 Z Z Z函数,实际上如果 s t r 1 str1 str1 等于 s t r 2 str2 str2,扩展 K M P KMP KMP求解的就是 Z Z Z函数,因此可以使用类似于 Z Z Z函数的方法求解

假定 e x t e n d [ i ] extend[i] extend[i]表示 s t r 1 str1 str1 i i i位开始的后缀与 s t r 2 str2 str2的最长公共前缀

同理:类似于 Z − b o x Z-box Zbox,我们维护一个区间 [ l , r ] [l,r] [l,r]保证

  • [ l , r ] [l,r] [l,r]随着 s t r 1 str1 str1中的 i i i移动,在位置 i i i时,必须包含 i i i
  • r r r尽可能的大

同理可以分好几种情况讨论

1、 z [ i − l + 1 ] < r − i + 1 z[i-l+1] < r-i+1 z[il+1]<ri+1

此时, e x t e n d [ i ] = z [ i − l + 1 ] extend[i] = z[i-l+1] extend[i]=z[il+1],区间 [ l , r ] [l,r] [l,r]保持不变。

2、 z [ i − l + 1 ] > = r − i + 1 z[i-l+1] >= r-i+1 z[il+1]>=ri+1

我们只能保证绿色区域相同,超出的部分需要收到更新,即 e x t e n d [ i ] extend[i] extend[i] r r r开始暴力扫描求解,区间 [ l , r ] [l,r] [l,r]变成 [ i , i + e x t e n d [ i ] − 1 ] [i, i+extend[i]-1] [i,i+extend[i]1]

3、 i > r i > r i>r时,无法使用已有信息进行匹配更新,直接从 i i i位置开始扫描求解,同时区间 [ l , r ] [l,r] [l,r]变成 [ i , i + e x t e n d [ i ] − 1 ] [i, i+extend[i]-1] [i,i+extend[i]1]

参考程序:

void get_ext (char *s1, int n, char *s2, int m) { // s1的每一个后缀与s2的lcp

    int l = 0, r = 0;
    for (int i = 1; i <= n; i ++) {
        if (i > r) {
            while (i + ext[i] <= n and ext[i] + 1 <= m and s1[i + ext[i]] == s2[ext[i] + 1]) ext[i] ++;
            l = i, r = i + ext[i] - 1;
        } 
        else if (z[i - l + 1] < r - i + 1) ext[i] = z[i - l + 1];
        else {
            ext[i] = r - i + 1;
            while (i + ext[i] <= n and ext[i] + 1 <= m  and s1[i + ext[i]] == s2[ext[i] + 1]) ext[i] ++;
            l = i, r = i + ext[i] - 1;
        }
    }
}

// 优化写法
inline void exkmp(char *s, int n, char *t, int m) {
	Z(t, m);
	for (int i = 1; i <= n; i++) p[i] = 0;
	for (int i = 1, l = 0, r = 0; i <= n; i++) {
		if (i <= r) p[i] = min(z[i-l+1], r - i + 1);
		while (i + p[i] <= n && s[i+p[i]] == t[p[i]+1]) ++p[i];
		if (i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
	}
}

还有一种解决问题的思路:

s t r 1 str1 str1的每一个后缀与 s t r 2 str2 str2 l c p lcp lcp,我们可以将 s t r 1 str1 str1拼接在 s t r 2 str2 str2后面,对新的字符串 s t r str str,求它的 Z Z Z函数即可!

那么对于原先 s t r 1 str1 str1 e x t [ i ] ext[i] ext[i]等价于 z [ i + s i z e ( s t r 2 ) ] z[i+size(str2)] z[i+size(str2)]


例题

题目描述

给定两个字符串 a , b a,b a,b,你要求出两个数组:

  • b b b z z z 函数数组 z z z,即 b b b b b b 的每一个后缀的 L C P LCP LCP 长度。
  • b b b a a a 的每一个后缀的 L C P LCP LCP 长度数组 p p p

对于一个长度为 n n n 的数组 a a a,设其权值为 xor ⁡ i = 1 n i × ( a i + 1 ) \operatorname{xor}_{i=1}^n i \times (a_i + 1) xori=1ni×(ai+1)

输入格式

两行两个字符串 a , b a,b a,b

输出格式

第一行一个整数,表示 z z z 的权值。

第二行一个整数,表示 p p p 的权值。

输入样例1

aaaabaa
aaaaa

输出样例

6
21

说明/提示

样例解释:

z = { 5   4   3   2   1 } z = \{5\ 4\ 3\ 2\ 1\} z={5 4 3 2 1} , p = { 4   3   2   1   0   2   1 } p = \{4\ 3\ 2\ 1\ 0\ 2\ 1\} p={4 3 2 1 0 2 1}

数据范围:

对于第一个测试点, ∣ a ∣ , ∣ b ∣ ≤ 2 × 1 0 3 |a|,|b| \le 2 \times 10^3 a,b2×103

对于第二个测试点, ∣ a ∣ , ∣ b ∣ ≤ 2 × 1 0 5 |a|,|b| \le 2 \times 10^5 a,b2×105

对于 100%100% 的数据, 1 ≤ ∣ a ∣ , ∣ b ∣ ≤ 2 × 1 0 7 1 \le |a|,|b| \le 2 \times 10^7 1a,b2×107,所有字符均为小写字母。

(此题在洛谷上测试,编号为 P 5410 P5410 P5410,开 O 2 O_2 O2可过。)


参考程序1

#include <iostream>
#include <cstring>
using namespace std;

const int N = 2e7 + 15;
typedef long long ll;

char a[N], b[N];
int n, m;	
ll z[N], ext[N];

void get_z (char *s, int n) {

    ll l = 0, r = 0;
    z[1] = n;
    for (int i = 2; i <= n; i ++) {
        if (i > r) {
            while (s[i + z[i]] == s[z[i] + 1]) z[i] ++;
            l = i, r = i + z[i] - 1;
        } 
        else if (z[i - l + 1] < r - i + 1) z[i] = z[i - l + 1];
        else {
            z[i] = r - i + 1;
            while (s[i + z[i]] == s[z[i] + 1]) z[i] ++;
            l = i, r = i + z[i] - 1;
        }
    }
}

void get_ext (char *s1, int n, char *s2, int m) { // s1的每一个后缀与s2的lcp

    ll l = 0, r = 0;
    for (int i = 1; i <= n; i ++) {
        if (i > r) {
            while (i + ext[i] <= n and ext[i] + 1 <= m and s1[i + ext[i]] == s2[ext[i] + 1]) ext[i] ++;
            l = i, r = i + ext[i] - 1;
        } 
        else if (z[i - l + 1] < r - i + 1) ext[i] = z[i - l + 1];
        else {
            ext[i] = r - i + 1;
            while (i + ext[i] <= n and ext[i] + 1 <= m  and s1[i + ext[i]] == s2[ext[i] + 1]) ext[i] ++;
            l = i, r = i + ext[i] - 1;
        }
    }


}

int main () {
    // freopen ("2.in", "r", stdin);
	cin >> (a + 1) >> (b + 1);

	n = strlen(a + 1), m = strlen (b + 1);
	get_z (b, m);
    ll t1 = 0;
    for (int i = 1; i <= m; i ++) 
        t1 ^= ((z[i] + 1) * i);
    cout << t1 << endl;
	get_ext (a, n, b, m);
    t1 = 0;
    for (int i = 1; i <= n; i ++) 
        t1 ^= ((ext[i] + 1) * i);
    cout << t1 << endl;
	return 0;
} 

两个串合并:

参考程序2

#include <iostream>
#include <cstring>
using namespace std;

const int N = 2e7 + 15;
typedef long long ll;

string a, b;
ll n, m;   
ll z[N * 2];

void get_z (string s, int n) {

    ll l = 0, r = 0;
    z[1] = n;
    for (int i = 2; i <= n; i ++) {
        if (i > r) {
            while (s[i + z[i]] == s[z[i] + 1]) z[i] ++;
            l = i, r = i + z[i] - 1;
        } 
        else if (z[i - l + 1] < r - i + 1) z[i] = z[i - l + 1];
        else {
            z[i] = r - i + 1;
            while (s[i + z[i]] == s[z[i] + 1]) z[i] ++;
            l = i, r = i + z[i] - 1;
        }
    }
}

int main () {
    // freopen ("2.in", "r", stdin);
    cin >> a >> b;


    n = a.length(), m = b.length();

    get_z (" " + b + a, m + n);
    ll t1 = 0;
    for (int i = 1; i <= m; i ++) 
        t1 ^= ((min(z[i], m - i + 1) + 1) * i);
    
    cout << t1 << endl;
    
    t1 = 0;
    for (int i =  1; i <= n; i ++) 
        t1 ^= ((min(z[i + m], m) + 1) * i);

    cout << t1 << endl;
    return 0;
} 
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值