ExKMP(Z Algorithm) 講解

問題引入

衆所周知,\(\mathrm{KMP}\) 算法是最爲經典的單模板字符串匹配問題的線性解法。那麼 \(\mathrm{ExKMP}\) 字面意義是 \(\mathrm{KMP}\) 的擴展,那麼它是解決什麼問題呢?c++

CaiOJ 1461 【EXKMP】最長共同前綴長度

存在母串 \(S\) 和子串 \(T\) ,設 \(|S| = n, |T| = m\) ,求 \(T\)\(S\) 的每個後綴的最長公共前綴 \((\mathrm{LCP})\)git

\(extend\) 數組, \(extend[i]\) 表示 \(T\)\(S_{i \sim n}\)\(\mathrm{LCP}\) ,對於 \(i \in [1, n]\)\(extend[i]\)算法

\(1 \le m \le n \le 10^6\)數組

如下的字符串下標均從 \(1\) 開始標號。spa

算法講解

本文參考了這位 大佬的講解.net

其實能夠直接用 \(SA / SAM\) 解決,可是太大材小用了。。。(但彷佛不太好作到 \(O(n)\) 有一種是作到 \(O(n) - O(1) \mathrm{RMQ}\) )debug

對於通常的 \(\mathrm{KMP}\) 只須要求全部 \(extend[i] = m\) 的位置,那麼 \(\mathrm{ExKMP}\) 就是須要求出這個 \(extend[i]\) 數組。code

舉個例子更好理解。blog

\(S = \underline{aaaabaa}, T = \underline{aaaaa}\)

S: a a a a b a a
   | | | | X
T: a a a a a

咱們知道 \(extend[1] = 4\) ,而後計算 \(extend[2]\) ,咱們發現從新匹配是很浪費時間的。

因爲 \(S_{1 \sim 4} = T_{1\sim 4}\) ,那麼 \(S_{2 \sim 4} = T_{2 \sim 4}\)

此時咱們須要一個輔助的匹配數組 \(next[i]\) 表示 \(T_{i \sim m}\)\(T\)\(\mathrm{LCP}\)

咱們知道 \(next[2] = 4\) ,那麼 \(T_{2 \sim 5} = T_{1 \sim 4} \Rightarrow T_{2 \sim 4} = T_{1 \sim 3}\)

因此能夠直接從 \(T_4\) 開始和 \(S_5\) 匹配,此時發現會失配,那麼 \(extend[2] = 3\)

這其實就是 \(\mathrm{ExKMP}\) 的主要思想,下面簡述其匹配的過程。

匹配過程

此處假設咱們已經獲得了 \(next[i]\)

當前咱們從前日後依次遞推 \(extend[i]\) ,假設當前遞推完前 \(k\) 位,要求 \(k + 1\) 位。

此時 \(extend[1 \sim k]\) 已經算完,假設以前 \(T\) 能匹配 \(S\) 的後綴最遠的位置爲 \(p = \max_{i < k} (i + extend[i] - 1)\) ,對應取到最大值的位置 \(i\)\(pos\)

S: 1 ... pos ... k k+1 ... p ...

那麼根據 \(extend\) 數組定義有 \(S_{pos \sim p} = T_{1 \sim p - pos + 1} \Rightarrow S_{k + 1 \sim p} = T_{k - pos + 2 \sim p -pos + 1}\)

\(len = next[k - pos + 2]\) ,分如下兩種狀況討論。

  1. \(k + len < p\)

    S: 1 ... pos ... k k+1 ... k+len k+len+1 ... p ...
                        |   |    |      X
    T:                  1  ...  len   len+1  ...

    此時咱們發現 \(S_{k + 1 \sim k + len} = T_{1 \sim len}\)

    因爲 \(next[k - pos + 2] = len\) 因此 \(T_{k + len + pos + 2} \not = T_{len + 1}\)

    又因爲 \(S_{k + len + 1} = T_{k + len - pos + 2}\) 因此 \(S_{k + len + 1} \not = T_{len + 1}\)

    這意味着 \(extend[k + 1] = len\)

  2. \(k + len \ge p\)

    S: 1 ... pos ... k k+1 ...  p  p+1   ... ...
                        |   |   |   ?
    T:                  1  ... ... p-k+2 ... len ...

    那麼 \(S_{p + 1}\) 以後的串咱們都從何嘗試匹配過,不知道其信息,咱們直接暴力向後依次匹配便可,直到失配停下來。

    若是 \(extend[k + 1] + k > p\) 要更新 \(p\)\(pos\)

next 的求解

前面咱們假設已經求出 \(next\) ,但如何求呢?

其實和 \(\mathrm{KMP}\) 是很相似的,咱們至關於 \(T\) 本身匹配本身每一個後綴的答案,此處須要的 \(next\) 全都在前面會計算過。

和前面匹配的過程是如出一轍的。

複雜度證實

下面來分析一下算法的時間複雜度。

  1. 對於第一種狀況,無需作任何匹配便可計算出 \(extend[i]\)

  2. 對於第二種狀況,都是從未被匹配的位置開始匹配,匹配過的位置再也不匹配,也就是說對於母串的每個位置,都只匹配了一次,因此算法整體時間複雜度是 \(O(n)\) 的。

代碼解決

注意 \(k + 1 = i\) ,不要弄錯下標了。

#include <bits/stdc++.h>

#define For(i, l, r) for (register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for (register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Rep(i, r) for (register int i = (0), i##end = (int)(r); i < i##end; ++i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
#define next Next

using namespace std;

template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }

inline int read() {
    int x(0), sgn(1); char ch(getchar());
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
    for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
    return x * sgn;
}

void File() {
#ifdef zjp_shadow
    freopen ("1461.in", "r", stdin);
    freopen ("1461.out", "w", stdout);
#endif
}

const int N = 1e6 + 1e3;

void Get_Next(char *S, int *next) {
    int lenS = strlen(S + 1), p = 1, pos;
    next[1] = lenS; // 對於 next[1] 要特殊考慮
    while (p + 1 <= lenS && S[p] == S[p + 1]) ++ p;
    next[pos = 2] = p - 1; // next[2] 是爲了初始化
 
    For (i, 3, lenS) { // 注意此時 k + 1 = i
        int len = next[i - pos + 1];
        if (len + i < p + 1) next[i] = len; // 對應上面第一種狀況
        else {
            int j = max(p - i + 1, 0); // 找到前面對於 子串 最靠後已經匹配的位置
            while (i + j <= lenS && S[j + 1] == S[i + j]) ++ j; // 第二種須要暴力匹配
            p = i + (next[pos = i] = j) - 1; // 記得更新 p, pos
        }
    }
}
 
void ExKMP(char *S, char *T, int *next, int *extend) {
    int lenS = strlen(S + 1), lenT = strlen(T + 1), p = 1, pos;
 
    while (p <= lenT && S[p] == T[p]) ++ p;
    p = extend[pos = 1] = p - 1; // 初始化 extend[1]
 
    For (i, 2, lenS) {
        int len = next[i - pos + 1];
        if (len + i < p + 1) extend[i] = len;
        else {
            int j = max(p - i + 1, 0);
            while (i + j <= lenS && j <= lenT && T[j + 1] == S[i + j]) ++ j;
            p = i + (extend[pos = i] = j) - 1;
        }
    } // 和上面基本如出一轍啦
}

char S[N], T[N]; int next[N], extend[N];

int main () {

    File();

    scanf ("%s", S + 1);
    scanf ("%s", T + 1);

    Get_Next(T, next);
    ExKMP(S, T, next, extend);

    For (i, 1, strlen(S + 1))
        printf ("%d%c", extend[i], i == iend ? '\n' : ' ');

    return 0;

}

一些例題

UOJ #5. 【NOI2014】動物園

題意

給你一個字符串 \(S\) ,定義 \(num\) 數組 --- 對於字符串 \(S\) 的前 \(i\) 個字符構成的子串,既是它的後綴同時又是它的前綴,而且 該後綴與該前綴不重疊 ,將這種字符串的數量記做 \(num[i]\)

\(\prod_{i = 1}^{|S|} (num[i] + 1) \pmod {10^{9}+7}\)

題解

若是會 \(\mathrm{ExKMP}\) 就是裸題了。

而後考慮對於每一個 \(S\) 的後綴 \(i\) 會被算多少遍,其實就是對於以 \([i, \min(2 \times (i - 1), i + next[i] - 1)]\) 爲結尾的全部前綴有貢獻,那麼直接差分便可。

複雜度是 \(O(\sum |S|)\) 的。

代碼

前面的板子就再也不放了。

const int N = 1e6 + 1e3, Mod = 1e9 + 7;

char str[N]; int num[N], next[N];
 
int main () {

    File();
    
    for (int cases = read(); cases; -- cases) {

        scanf ("%s", str + 1); Set(num, 0);
        Get_Next(str, next);

        int n = strlen(str + 1);
        For (i, 2, n)
            if (next[i])
                ++ num[i], -- num[min(i * 2 - 1, i + next[i])];

        int ans = 1;
        For (i, 1, n)
            ans = 1ll * ans * ((num[i] += num[i - 1]) + 1) % Mod;
        printf ("%d\n", ans);

    }

    return 0;

}

CF1051E Vasya and Big Integers

題意

給你一個由數字構成的字符串 \(a\) ,問你有多少種劃分方式,使得每段不含前導 \(0\) ,而且每段的數字大小在 \([l, r]\) 之間。答案對於 \(998244353\) 取模。

\(1 \le a \le 10^{1000000}, 0 \le l \le r \le 10^{1000000}\)

題解

考慮暴力 \(dp\) ,令 \(dp_i\) 爲以 \(i\) 爲一段結束的方案數。對於填表法是沒有那麼好轉移的,(由於前導 \(0\) 的限制是掛在前面那個點上)咱們考慮刷表法。

那麼轉移爲
\[ dp_j = dp_j + dp_i~~\{j~|~a_i \not = 0 \& l \le a_{i \sim j} \le r\} \]

咱們發現 \(dp_i\) 能轉移到的 \(j\) 必定是一段連續的區間。

咱們就須要快速獲得這段區間,首先不難發現 \(j\) 對應的位數區間是能夠很快肯定的,就是 \([l + |L| - 1, i + |R| - 1]\)

可是若是位數同樣的話須要多花費 \(O(n)\) 的時間去逐位比較大小。

有什麼快速的方法嗎?不難想到比較兩個數字大小的時候是和字符串同樣的,就是 \(\mathrm{LCP}\) 的後面一位。

那麼咱們用 \(\mathrm{ExKMP}\) 快速預處理 \(extend(\mathrm{LCP})\) 就能夠了。

代碼

const int N = 1e6 + 1e3, Mod = 998244353;

inline void Add(int &a, int b) {
    if ((a += b) >= Mod) a -= Mod;
}

char S[N], L[N], R[N];

template<typename T>
inline int dcmp(T lhs, T rhs) {
    return (lhs > rhs) - (lhs < rhs);
}

inline int Cmp(int l, int r, char *cmp, int *Lcp, int len) {
    if (r - l + 1 != len) return dcmp(r - l + 1, len);
    return l + Lcp[l] > r ? 0 : dcmp(S[l + Lcp[l]], cmp[Lcp[l] + 1]);
}

int lenL, lenR, tmp[N], EL[N], ER[N];

inline bool Check(int x, int y) {
    return Cmp(x, y, L, EL, lenL) >= 0 && Cmp(x, y, R, ER, lenR) <= 0;
}

int tag[N], dp = 1;

int main () {

    File();

    scanf ("%s", S + 1);
    int n = strlen(S + 1);

    scanf ("%s", L + 1); lenL = strlen(L + 1); Get_Next(L, tmp); ExKMP(S, L, tmp, EL);
    scanf ("%s", R + 1); lenR = strlen(R + 1); Get_Next(R, tmp); ExKMP(S, R, tmp, ER);

    tag[1] = Mod - 1;
    For (i, 1, n) {
        int l, r;
        if (S[i] == '0') {
            if (L[1] == '0') l = r = i;
            else { Add(dp, tag[i]); continue; }
        } else {
            l = i + lenL - 1; if (!Check(i, l)) ++ l;
            r = i + lenR - 1; if (!Check(i, r)) -- r;
        }
        if (l <= r) Add(tag[l], dp), Add(tag[r + 1], Mod - dp); 
        Add(dp, tag[i]);
    }

    printf ("%d\n", (dp + Mod) % Mod);

    return 0;

}
相關文章
相關標籤/搜索