前缀函数和KMP算法

前缀函数(π函数)

定义

border :若字符串 s 存在某个真前缀和某个真后缀相同,则这个真前缀或真后缀称为 s 的一个 border。\

前缀函数 :前缀函数 π[i] 的值为字符串 s 的前缀 s[0,i] 的最长 border 的长度。特别地, π[0]=0

实现

vector<int> prefix_function(string s) {
    int n = (int)s.length();
    vector<int> pi;
    for (int i = 1; i < n; i++) {
        int j = pi[i - 1];
        while (j && s[i] != s[j]) j = pi[j - 1];
        if (s[i] == s[j]) j++;
        pi[i] = j;
    }
    return pi;
}

前缀函数的应用

一 、模式匹配(KMP算法)

给定一个文本串 t 和一个模式串 s ,找出 st 中出现的所有位置。

将模式串与文本串拼接,在中间加一个分隔符,变为字符串 s+#+t ,然后对这个字符串求出前缀函数,每个前缀函数等于 |s| 的位置即为模式串出现的位置的末尾。

二 、字符串的周期

对字符串 s ,若存在 0<p<|s| ,使得 s[i]=s[i+p] 对所有 i[0,|s|p1] 成立,则称 p 为字符串 s 的周期。

s 有长度为 r 的 border ,则 |s|rs 的周期。

可以借助前缀函数得到 s 的所有 border ,即 π[n1],π[π[n1]1],... 以此类推。于是可以得到 s 的所有周期,其中 s 的最小周期为 nπ[n1]

三 、统计每个前缀的出现次数

考虑位置 i 的前缀函数值 π[i] 。根据定义,其意味着字符串 s 的一个长度为 π[i] 的前缀在位置 i 出现并以 i 为右端点,同时不存在一个更长的前缀满足前述定义。与此同时,更短的前缀可能以该位置为右端点。

所以我们可以通过以下方法计算答案 。

for (int i = 0; i < n; i++) ans[pi[i]]++;
for (int i = n - 1; i > 0; i--) ans[pi[i-1]] += ans[i];
for (int i = 0; i < n; i++) ans[i]++;

四 、一个字符串中本质不同子串的数目

考虑向一个字符串 s 末尾添加一个字符 c 后出现的新的字串数目。

构造字符串 t=s+c ,并将其反转成为 t ,问题变为求 t 有多少前缀未在其他地方出现过。如果计算出 t 的前缀函数最大值 πmax ,那么长度大于 πmax 的前缀都没有出现过,故添加了一个新字符后新出现的子串数目为 |s|+1πmax

五 、字符串压缩(字符串的整周期)

给定一个长度为 n 的字符串 s ,我们希望找到其最短的“压缩”表示,也即我们希望寻找一个最短的字符串 t ,使得 s 可以被 t 的一份或多份拷贝的拼接表示。

计算 s 的前缀函数。我们定义值 k=nπ[n1] 。如果 k 整除 n ,那么前缀 s[0,k1] 就是答案,否则可以证明不存在一个有效的压缩,故答案为 s

六 、根据前缀函数构建一个自动机

重新考虑计算前缀函数的问题,我们先前使用构造字符串 s+#+t 的方法。但如果字符串 t 是某种巨大的字符串,我们只知道它的构造方式,无法将它显式地构造出来,那么如何计算?

例如计算字符串 sk 阶 Gray 字符串中出现的次数,Gray 字符串的定义为:

g1=a

g2=g1+b+g1

g3=g2+c+g2

......

gk=gk1+Σk+gk1

k 个字符串地长度达到了 2k 的数量级,无法通过之前的方法计算。

先计算 s+# 的前缀函数,因为 # 不属于字符集,所以之后的前缀函数值一定不超过 |s| 。我们把前缀函数值当作状态,字符当作转移的条件,构造出自动机。

自动机部分代码

void compute_automaton(string s, vector<vector<int>> &aut) {
    s += '#';
    int n = (int)s.size();
    vector<int> pi = prefix_function(s);
    aut.assign(n, vector<int>(26));
    for (int i = 0; i < n; i++) {
        for (int c = 0; c < 26; c++) {
            if (i > 0 && 'a' + c != s[i]) aut[i][c] = aut[pi[i - 1]][c];
            else aut[i][c] = i + ('a' + c == s[i]);
        }
    }
}

我们实际要计算的是有多少个位置的前缀函数值等于 |s| ,故可以使用 dp 。

G[i][j] 表示,从状态 j 开始处理完第 i 个字符串后所去到的自动机的状态。同时设 K[i][j] 表示从状态 j 开始处理完第 i 个字符串后,s 出现的次数。这两个数组不难计算。

初始条件

G[0][j]=j

K[0][j]=0

转移

mid=aut[G[i1][j]][i]

G[i][j]=G[i1][mid]

K[i][j]=K[i1][j]+[mid==|s|]+K[i1][mid]

最终答案就是 K[𝑘][0]

posted @   imyhy  阅读(71)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示