Z函数/拓展KMP

思想

z i z_i zi的定义是, i i i开头的后缀,和字符串本身的最长公共前缀。

和 k m p kmp kmp的类似点是,都能利用前面递推的结果,加速当前的计算,从而把 O ( n 2 ) O(n^2) O(n2)加速到 O ( n ) O(n) O(n)。当然加速的方式不太一样。

核心是,假设当前处理完一个后缀的答案,那么这个后缀对应一个 z b o x zbox zbox,也就是一个区间 [ i , i + z [ i ] − 1 ] [i,i+z[i]-1] [i,i+z[i]−1]。在递推过程中,维护一个右端点最大的区间,作为 z b o x zbox zbox。

对于一个 i i i,如果他在前面推出的 z b o x zbox zbox里,那么首先根据 z z z函数的定义, z b o x zbox zbox的区间 [ l , r ] [l,r] [l,r],和前缀 [ 0 , r − l ] [0,r-l] [0,r−l]是相等的,那么 i i i的 z z z函数,至少和 z [ i − l ] z[i-l] z[i−l]是一样的,当然,不能长度超过 r − i + 1 r-i+1 r−i+1,因为我们目前只能保证 [ i − l , r − l ] [i-l,r-l] [i−l,r−l]和 [ i , r ] [i,r] [i,r]这两段是一样的,可以复用结果,出了这个范围,则只能暴力匹配。

这哥的复杂度保证,是在于我们在当前回文不超出 r r r的时候,不会暴力匹配,而暴力匹配每次都会增加 r r r, r r r单调递增,最多增加 O ( n ) O(n) O(n)次,所以里面那个 w h i l e while while循环实际上只会 O ( n ) O(n) O(n)复杂度。

代码

两种实现,第二种是详细分类讨论。

第一种规避了分讨

c 复制代码
vector<int> calc_z(const string& s) {
    int n = s.size();
    vector<int> z(n);
    int box_l = 0, box_r = 0;
    for (int i = 1; i < n; i++) {
        if (i <= box_r) {
            z[i] = min(z[i - box_l], box_r - i + 1);
        }
        while (i + z[i] < n && s[z[i]] == s[i + z[i]]) {
            box_l = i;
            box_r = i + z[i];
            z[i]++;
        }
    }
    z[0] = n;
    return z;
}


vector<int> z_function(const string &s) {
  int n = (int)s.length();
  vector<int> z(n);
  for (int i = 1, l = 0, r = 0; i < n; ++i) {
    if (i <= r && z[i - l] < r - i + 1) {
      z[i] = z[i - l];
    } else {
      z[i] = max(0, r - i + 1);
      while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i];
    }
    if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
  }
  return z;
}

例题

28. 找出字符串中第一个匹配项的下标

z函数也能做字符串匹配,把模式串p,文本串t,拼成p#s,这样我们z函数求的,一个后缀和整个串的lcp,实际上就是求的文本串的一个后缀,和模式串的匹配长度,如果这个匹配长度为模式串长度则为找到一个匹配

c 复制代码
vector<int> calc_z(const string& s) {
    int n = s.size();
    vector<int> z(n);
    int box_l = 0, box_r = 0;
    for (int i = 1; i < n; i++) {
        if (i <= box_r) {
            z[i] = min(z[i - box_l], box_r - i + 1);
        }
        while (i + z[i] < n && s[z[i]] == s[i + z[i]]) {
            box_l = i;
            box_r = i + z[i];
            z[i]++;
        }
    }
    z[0] = n;
    return z;
}
class Solution {
public:
    int strStr(string t, string p) {
        string s = p + "#" + t;
        vector<int> z = calc_z(s);

        int n = p.size();
        for (int i = 0; i + n - 1 < t.size(); i++) {
            if (z[i + n + 1] == n) {
                return i;
            }
        }
        return -1;
    }
};

3031. 将单词恢复初始状态所需的最短时间 II

添加到末尾的可以随意添加,我们当然可以让添加的等于初始状态的后缀。所以问题就是剩下的前缀,能否等于初始的前缀。剩下的前缀实际上是初始的一个后缀。

那么就是问一个串的后缀能否等于等长的前缀,求z函数,看一个后缀的z函数值,是否和这个后缀长度相等。

注意到这也是一个前缀和后缀的匹配,也可以kmp求最长公共前后缀实现。

求出z函数后,枚举所有长度为k的倍数的后缀,找到符合条件的,最长的后缀,就对应最少的删除次数。如果所有长度k倍数的后缀都不符合,那么只能让这个操作把整个串都移除了,然后通过添加得到初始串

c 复制代码
vector<int> calc_z(const string& s) {
    int n = s.size();
    vector<int> z(n);
    int box_l = 0, box_r = 0;
    for (int i = 1; i < n; i++) {
        if (i <= box_r) {
            z[i] = min(z[i - box_l], box_r - i + 1);
        }
        while (i + z[i] < n && s[z[i]] == s[i + z[i]]) {
            box_l = i;
            box_r = i + z[i];
            z[i]++;
        }
    }
    z[0] = n;
    return z;
}
class Solution {
public:
    int minimumTimeToInitialState(string word, int k) {
        int n = word.size();
        vector<int> z = calc_z(word);

        for (int i = k; i < n; i += k) {
            if (z[i] == n - i) {
                return i / k;
            }
        }

        return (n + k - 1) / k;
    }
};

3045. 统计前后缀下标对 II

kmp+字典树

给个字符串数组,问有多少对字符串,s同时是t的前缀和后缀。首先t的某个长度的前后缀相同,才可能存在s和t配对,所以先预处理t的z函数,通过z函数检查每一个后缀,是否存在一个相等的前缀,如果存在检查有多少s,等于这个前缀。

检查某个s的个数,可以字符串哈希,然后对哈希值计数。也可以trie在节点上记录每个点结束的字符串个数end_cnt。

这里如果用字典树的话,需要在字典树上搜索t的过程中,每搜索到一个前缀,就检查z函数,判断是否前后缀相等,如果相等,累加这个节点的end_cnt。

为了防止重复计数,可以采用扫描线的做法,每次计算完一个t的答案,把这个t作为s插入字典树,也就是在结束节点end_cnt++。

3303. 第一个几乎相等子字符串的下标

对于文本串中,每个长度等于模式串的子串,检查它是否是几乎相等。可以让这个子串的前缀和模式串的前缀求lcp,子串的后缀和模式串的后缀求lcp,如果长度加起来不小于m-1,也就是至多有一个不匹配的地方,则可以通过修改至多一个匹配上。

找子串的前缀和模式串的lcp,就是找文本串的一个后缀和模式串的lcp,把模式串接到文本串前面跑z函数即可,类似字符串匹配的处理。对于子段后缀和文本串后缀的lcp,把模式串和文本串都反转,再拼接求z函数即可

3292. 形成目标字符串需要的最少字符串数 II

z函数+跳跃游戏

给一个字符串数组,用里面任意字符串的任意前缀,问拼接出目标串的最少拼接次数?

假设当前已经求了 [ 0 , i − 1 ] [0,i-1] [0,i−1]的拼接次数,接下来从 i i i开始拼接,如果能知道 i i i开始能拼接的最大长度,由于前缀的前缀仍然是前缀,肯定小于这个最大长度的拼接也存在。那么这就可以转化成跳跃游戏贪心。

现在问题就是,对于每个位置i,能拼接的最大长度?这等价于求i开头的后缀,和所有模式串的最大lcp。让目标串和每个模式串都跑一次z函数,然后取最大即可。

c 复制代码
vector<int> calc_z(const string& s) {
    int n = s.size();
    vector<int> z(n);
    int box_l = 0, box_r = 0;
    for (int i = 1; i < n; i++) {
        if (i <= box_r) {
            z[i] = min(z[i - box_l], box_r - i + 1);
        }
        while (i + z[i] < n && s[z[i]] == s[i + z[i]]) {
            box_l = i;
            box_r = i + z[i];
            z[i]++;
        }
    }
    z[0] = n;
    return z;
}

int jump(vector<int>& a) {
    int ans = 0, mx = 0, lim = 0;
    for (int i = 0; i < a.size(); i++) {
        mx = max(mx, i + a[i]);
        if (i == lim) {
            if (i == mx) {
                return -1;
            }
            ans++;
            lim = mx;
        }
    }
    return ans;
}
class Solution {
public:
    int minValidStrings(vector<string>& words, string target) {
        int n = target.size();
        vector<int> mx(n);
        for (auto& s : words) {
            vector<int> z = calc_z(s + "#" + target);
            int m = s.size() + 1;
            for (int i = 0; i < n; i++) {
                mx[i] = max(mx[i], z[i + m]);
            }
        }
        return jump(mx);
    }
};

3388. 统计数组中的美丽分割

数据允许 O ( n 2 ) O(n^2) O(n2),枚举两个切点即可,接下来检查s1是不是s2前缀,s2是不是s3前缀,可以字符串哈希,也可以z函数s1是不是s2前缀,对初始串跑一次z函数即可,因为我们这就是要把s1当模式串,正好s1就是初始串的前缀。

s2是不是s3前缀,需要把s2开头的后缀拿出来跑一次z函数,因为需要让s2当模式串,在最前面。

2430. 对字母串可执行的最大删除数

z函数 dp优化

每次删除一个前缀,剩下的子问题是一个更小的后缀,所以 d p dp dp定义是 f i f_i fi表示删掉 i i i开头的后缀的最大操作数,递推的话,需要从较小子问题推到较大子问题,也就需要 i i i从大到小枚举,相当于从小到大枚举后缀。

转移是,如果 [ i , i + j − 1 ] = [ i + j , i + 2 j ] [i,i+j-1]=[i+j,i+2j] [i,i+j−1]=[i+j,i+2j],那么可以把 [ i , i + j − 1 ] [i,i+j-1] [i,i+j−1]删掉, f i = f i + j + 1 f_i=f_{i+j}+1 fi=fi+j+1

判断 [ i , i + j − 1 ] = [ i + j , i + 2 j ] [i,i+j-1]=[i+j,i+2j] [i,i+j−1]=[i+j,i+2j]这里,可以字符串哈希,也可以对 i i i开头的后缀跑一次z函数。

相关推荐
追随者永远是胜利者2 小时前
(LeetCode-Hot100)39. 组合总和
java·算法·leetcode·职场和发展·go
追随者永远是胜利者2 小时前
(LeetCode-Hot100)34. 在排序数组中查找元素的第一个和最后一个位置
java·算法·leetcode·职场和发展·go
键盘鼓手苏苏4 小时前
Flutter for OpenHarmony:markdown 纯 Dart 解析引擎(将文本转化为结构化 HTML/UI) 深度解析与鸿蒙适配指南
前端·网络·算法·flutter·ui·html·harmonyos
郝学胜-神的一滴5 小时前
当AI遇见架构:Vibe Coding时代的设计模式复兴
开发语言·数据结构·人工智能·算法·设计模式·架构
Frostnova丶10 小时前
LeetCode 190.颠倒二进制位
java·算法·leetcode
骇城迷影10 小时前
代码随想录:链表篇
数据结构·算法·链表
专注前端30年11 小时前
智能物流路径规划系统:核心算法实战详解
算法
json{shen:"jing"}11 小时前
字符串中的第一个唯一字符
算法·leetcode·职场和发展
追随者永远是胜利者12 小时前
(LeetCode-Hot100)15. 三数之和
java·算法·leetcode·职场和发展·go