20260723 - KMP

可惜 std::find 不是 \(O(n+m)\) 的算法啊。(注意不是 KMP)

暴力匹配

暴力匹配就是把模式串在文本串的每一个起始位置上,完整地比对一遍。

简单的来说,就是只要某个位置比对失败了,就把模式串整体往右移动一位,从头开始重新比对,直到走完为止(内层走完模式串算匹配成功,外层走完所有可能的起始位置算搜索结束)。

时间复杂度:\(O(n \times m)\)

空间复杂度:\(O(1)\)(以下都记录算法复杂度)。

这个算法平均跑的很快,但是容易被卡。

std::find 可以在最坏 \(O(n \times m)\) 内进行字符串匹配,但平均 \(O(n+m)\)


Hash 优化

说的没错,在暴力匹配的基础上,用 Hash 优化。

每次移动的代价是 \(O(1)\) 的,所以最后的复杂度是 \(O(n+m)\)

但是注意 Hash 是可能被卡的,所以有更好的 KMP 算法。

PS:我们常说的字符串匹配 Hash 优化,其正式的算法名称是 Rabin-Karp 算法。


KMP 算法

KMP 能在线性时间复杂度进行单模式串匹配。

首先有一个 \(next\) 函数,先别管怎么求解的,他表示一个串的最长公共前后缀(border)。

如果能求出一个串的 border,就能把两个串拼起来来匹配了。

就比如 \(S = \text{ababababababcab}\)\(S = \text{abababc}\),可以先拼成 \(F = \text{abababc\$ababababababcab}\),然后再对 \(F\) 求 border。


求 border

情况一:匹配成功

\(F_{i+1} = F_{j+1}\)(且 \(j < m\)),则匹配长度增加 \(1\),此时文本指针前进一位。

情况二:匹配失败

\(F_{i+1} \ne F_{j+1}\),则当前字符无法扩展匹配。此时:

  • 文本指针 \(i\) 保持不动,不回溯;
  • \(j = \text{next}_j\),将 \(j\) 回退到次长的可匹配前缀长度;
  • 重复比较同一个 \(F_{i+1}\) 与新的 \(F_{j+1}\),直到 \(j = 0\) 或匹配成功为止。

注意:令 \(j = \text{next}_j\) 的时候,可能不止跳转一次。

我们发现,如果这样,第一个指针就不用回退了。

所以,时间复杂度是 \(O(n+m)\)


代码:

vector<int> next_algorithm(const string &b) {
    const string &s = '_' + b;
    int len = (int)b.size();
    vector<int> nxt(len + 1);
    nxt[1] = 0; int j = 0;
    for (int i = 2; i <= len; i++) {
        while (j && s[i] != s[j + 1]) j = nxt[j];
        if (s[i] == s[j + 1]) ++j;
        nxt[i] = j;
    }
    return nxt;
}

vector<int> kmp(const string &a, const string &b) {
    const string &s = b + '#' + a;
    return next_algorithm(s);
}

例题

前言:exKMP 是个好东西,久违的用上 std::cin 了好吧。

B - Radio Transmission 无线传输

发现答案是 \(n - \text{next}_n\)

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int n;
    string a;
    cin >> n >> a;
    const auto &nxt = next_algorithm(a);
    printf("%d\n", (int)a.size() - nxt.back());
    return 0;
}

C - MUH and Cube Walls

在差分数组上做 KMP。

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
        scanf("%d", &x[i]);
    for (int i = 1; i <= m; i++)
        scanf("%d", &y[i]);
    if (m == 1) return printf("%d\n", n), 0;
    for (int i = 1; i < n; i++)
        a[i] = x[i + 1] - x[i];
    for (int i = 1; i < m; i++)
        b[i] = y[i + 1] - y[i];
    for (int i = 1; i <= n; i++) debug(a[i]);
    for (int i = 1; i <= m; i++) debug(b[i]);
    kmp();
    return 0;
}

D - Compress Words

找重叠即可,注意只要取后缀。

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int n;
    string pre, s;
    cin >> n;
    string ans;
    for(int i = 1;i <= n;i++){
        cin >> s;
        if (i == 1) ans = s;
        else {
            int l1 = (int)ans.size();
            int l2 = (int)s.size();
            const auto &nxt = kmp(ans.substr(l1 - min(l1,l2)), s);
            ans += s.substr(nxt.back());
        }
    }
    cout << ans << '\n';
    return 0;
}

E - OKR-Periods of Words

找到最小的然后减去就变成最大的了。

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int n;
    string s;
    cin >> n >> s;
    const auto &nxt = next_algorithm(s);
    vector<int> ne = nxt;
    s = '_' + s;
    ll ans = 0;
    for (int i = 2; i <= n; i++) {
        int j = i;
        while (ne[j]) j = ne[j];
        if (nxt[i])
            ne[i] = j;
        ans += i - j;
    }
    printf("%lld\n", ans);
    return 0;
}

F - Password

讲讲赛时做法吧。

首先求一遍 kmp 和$exKMP。

找到最大的,然后再跑一边 KMP。

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    string s;
    cin >> s;
    const auto &z = z_algorithm(s);
    const auto &nxt = next_algorithm(s);
    int maxx = -1, id = -1919810, n = (int)s.size();
    for (int i = 1; i <= n; i++) {
        if (z[i] > maxx && i + z[i] - 1 <= n) {
            maxx = z[i];
            id = i;
        }
    }
    if (maxx == -1) return puts("Just a legend"), 0;
    const string &t = s.substr(id - 1);
    const auto &kmp = next_algorithm(t);
    int len = (int)t.size();
    int ans = min(nxt[n], kmp[len]);
    if (!ans) puts("Just a legend");
    else for (int i = 0; i < ans; i++) printf("%c", t[i]);
    return 0;
}

KMP

把上面的 exKMP 替换成 KMP 就好了。

exkmp

判断长度就好了。

G - Prefixes and Suffixes

exKMP 神力。

首先求出 Z 函数,然后统计,差分求解即可。

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    string s;
    cin >> s;
    const auto &z = z_algorithm(s);
    int n = (int)s.size();
    for (int i = 1; i <= n; i++) {
        ++cnt[1];
        --cnt[z[i] + 1];
    }
    for (int i = 1; i <= n; i++) cnt[i] += cnt[i - 1];
    vector<PII> ans;
    for (int i = 1; i <= n; i++) {
        if (z[n - i + 1] == i) {
            ans.push_back({i, cnt[i]});
        }
    }
    printf("%d\n", (int)ans.size());
    for (auto [x, y] : ans) printf("%d %d\n", x, y);
    return 0;
}

PS:唯一一道题要人机验证的,痛失首 A(卡了20 min)。

H - Test

当然要枚举几种情况。

经过面向数据调试,终于过了。

就是可能有包含的情况。

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    string s[3];
    cin >> s[0] >> s[1] >> s[2];
    array<int, 3> arr{0, 1, 2};
    int ans = inf;
    do {
         auto solve = [&]() {
            string res;
            int nk = 0;
            for (int i = 0; i < 3; i++) {
                if (i == 0) res = s[arr[i]];
                else {
                    int l1 = (int)res.size();
                    int l2 = (int)s[arr[i]].size();
                    const auto &nxt = kmp(res.substr(l1 - min(l1,l2)), s[arr[i]]);
                    const string &now = s[arr[i]].substr(nxt.back());
                    res += now;
                }
            }
             int n = (int)s[arr[0]].size(), m = (int)s[arr[1]].size(), ok = false;
             const auto &nxt = kmp(s[arr[0]], s[arr[1]]);
             for (int i = m + 2; i <= n + m + 1; i++) {
                 if (nxt[i] == m) ok = true;
             }
             if (ok) {
                 res = "";
                 for (int i = 0; i < 3; i++) {
                     if (i == 1) continue;
                     if (i == 0) res = s[arr[i]];
                     else {
                         int l1 = (int)res.size();
                         int l2 = (int)s[arr[i]].size();
                         const auto &nxt1 = kmp(res.substr(l1 - min(l1,l2)), s[arr[i]]);
                         const string &now = s[arr[i]].substr(nxt1.back());
                         res += now;
                     }
                 }
                 return res.size();
             }
            return res.size();
         };
        ans = min<int>(ans, solve());
    } while (next_permutation(all(arr)));
    printf("%d\n", ans);
    // string l = "wlwobhnmmgtfolfaeckufuzdlsjxmevrtessfbwlnzzclcqwevnnucxyvhngnxhcbdfwqbrnnglylydxtgtvrlmeeszoiuatzzzx";
    // printf("%d\n", (int)l.size());
    // string l = "brnnglylydxtgtvrlmeeszoiuatzzzx";
    // printf("%d\n", (int)l.size());
    return 0;
}
// wlwobhnmmgtfolfaeckufuzdlsjxmevrtessfbwlnzzclcqwevnnucxyvhngnxhcbdfwqbrnnglylydxtgtvrlmeeszoiuatzzzx

I - 动物园

论看题的重要性。

KMP,然后往前跳,然后没了?

int main() {  
    ios::sync_with_stdio(0);  
    cin.tie(0), cout.tie(0);  
    int oT_To = 1;  
    cin >> oT_To;  
    while (oT_To--) {  
        string s;  
        cin >> s;  
        int n = (int)s.size();  
        const auto &nxt = next_algorithm(s);  
        ll ans = 1;  
        s = '_' + s;  
        int j = 0;  
        for (int i = 2; i <= n; i++) {  
            while (j && s[i] != s[j + 1]) j = nxt[j];  
            if (s[i] == s[j + 1]) ++j;  
            while (j >= i - j + 1) j = nxt[j];  
            ans = (ans * (p[j] + 1)) % P;  
        }  
        printf("%lld\n", ans);  
    }  
    return 0;  
}

J - Om Nom and Necklace

KMP 太困难了,让我们用 exKMP 秒掉他。

大概思路:将原序列分成 \(k\) 个块,每个块中的每一个元素可以用 \(z\) 函数判断(具体见代码注释),然后用差分维护。

代码:

vector<int> z_algorithm(const string &a) { // 求 Z 函数,不会的可以看看模板。
    const string &c = '_' + a;  
    int n = (int)c.size() - 1;  
    vector<int> z(n + 1);  
    int L = 1, R = 0;  
    z[1] = n;  
    for (int i = 2; i <= n; i++) {  
        if (i > R) z[i] = 0;  
        else {  
            int k = i - L + 1;  
            z[i] = min(z[k], R - i + 1);  
        }  
        while (i + z[i] <= n && c[z[i] + 1] == c[i + z[i]]) ++z[i];  
        if (i + z[i] - 1 > R) {  
            R = i + z[i] - 1;  
            L = i;  
        }  
    }  
    return z;  
}

int main() {  
    ios::sync_with_stdio(0);  
    cin.tie(0), cout.tie(0);  
    int n, k;  
    string s;  
    cin >> n >> k >> s;  
    const auto &z = z_algorithm(s);  
    s = '_' + s;  
    vector<int> d(n + 10);  
    for (int len = 1; len <= n / k; len++) { // 最多有 n / k 个块。
        bool ok = true;  
        for (int i = 1; i < k; i++) { // 枚举块中的元素,如果可以匹配的都已经小于 len,就一定不行。
            if (len * i + 1 <= n && z[len * i + 1] < len) {   
                ok = false;  
                break;  
            }  
        }  
        if (ok) { // 如果可以的话,可以凑出来的起点是 len * k,终点就是分的块的长度和下一个能匹配上的最小值。
            ++d[len * k];  
            --d[len * k + min(len, z[len * k + 1]) + 1];  
        }  
    }  
    for (int i = 1; i <= n; i++) d[i] += d[i - 1];  
    for (int i = 1; i <= n; i++) printf("%d", d[i] > 0); // 这里有可能不是 1,所以得判断 d[i] > 0。
    return 0;  
}

后记

exKMP 得了 MVP。

posted @ 2026-07-23 21:41  AKCoder  阅读(17)  评论(0)    收藏  举报