前缀函数和KMP算法

前缀函数(\(\pi\)函数)

定义

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

前缀函数 :前缀函数 \(\pi[i]\) 的值为字符串 \(s\) 的前缀 \(s[0,i]\) 的最长 border 的长度。特别地, \(\pi[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\) ,找出 \(s\)\(t\) 中出现的所有位置。

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

二 、字符串的周期

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

\(s\) 有长度为 \(r\) 的 border ,则 \(|s| -r\)\(s\) 的周期。

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

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

考虑位置 \(i\) 的前缀函数值 \(\pi[i]\) 。根据定义,其意味着字符串 \(s\) 的一个长度为 \(\pi[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’\) 的前缀函数最大值 \(\pi_{max}\) ,那么长度大于 \(\pi_{max}\) 的前缀都没有出现过,故添加了一个新字符后新出现的子串数目为 \(|s|+1−\pi_{max}\)

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

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

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

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

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

例如计算字符串 \(s\)\(k\) 阶 Gray 字符串中出现的次数,Gray 字符串的定义为:

\[g_1=a \]

\[g_2=g_1+b+g_1 \]

\[g_3=g_2+c+g_2 \]

\[...... \]

\[g_k=g_{k−1}+\Sigma_k+g_{k−1} \]

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

先计算 \(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[i−1][j]][i] \]

\[G[i][j]=G[i−1][mid] \]

\[K[i][j]=K[i−1][j]+[mid==|s|]+K[i−1][mid] \]

最终答案就是 \(K[𝑘][0]\)

posted @ 2024-02-25 15:59  imyhy  阅读(157)  评论(0)    收藏  举报