首页 > 其他 > 详细

KMP刷题记录

时间:2020-04-09 17:28:17      阅读:77      评论:0      收藏:0      [点我收藏+]

简介

这就是用来记录我对于《信息学奥赛一本通 · 提高篇》一书中的习题的刷题记录以及学习笔记。

一般分专题来写(全部写一起可能要加载很久...),比如本章就是用来记录KMP的。

再插一句:Loj 真是个不错的 OJ,如果说洛谷是最棒的 OIer 社区,那 Loj 就是最棒的刷题专区。

PS:这里的“最棒的”仅仅指带给我的练习感觉最好,例如画风好康,且仅代表个人意见。

专题介绍:KMP,快速单模匹配算法,反正很有用就对了。对于初学者来说,看似苦涩难懂,实则十分简单。

如果不会可以康康我的文章(自己去主页找),记住,有时并不需要全部的 KMP,而是其 \(next[]\) 的巧妙运用。

还有,仅仅会背模板是不够的,必须牢固掌握其原理才能做到做题时游刃有余。

第一题

#10043 「一本通 2.2 例 1」剪花布条

KMP 模板题?不存在的。

如果你开开心心的交了一个 KMP 模板就会发现自己在样例 2 (aaaaaa aa)时就 GG 了。

因为这里的查询是删除式的,什么意思?就是找到一个就删除一个,所以样例 2 的答案为 3。

那怎么办?各种书上都没有讲啊(有原题的书籍除外)。

这里就要求你掌握 KMP 的本质啦。在计算答案时,一般的程序时酱紫的:

void kmp() {
    for (int i = 1, j = 0; i <= n; i++) {
        while (j > 0 && (j == n || a[i] != b[j + 1])) j = nxt[j];
        if (a[i] == b[j + 1])
            ++j;
        if (j == m) ++ans;
    }
    return;
}

但是这肯定不行啊,那怎么办?看下面代码:

void kmp() {
    for (int i = 1, j = 0; i <= n; i++) {
        while (j > 0 && (j == n || a[i] != b[j + 1])) j = nxt[j];
        if (a[i] == b[j + 1])
            ++j;
        if (j == m) {
            ++ans;
            j = 0;//就多了一行。
        }
    }
    return;
}

这是什么意思呢?

\(j=0\) 后,就相当于排除前面的字符串,直接从头重新开始匹配,从而实现删除操作。

一下是完整代码:

#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iostream>
#define N 1010
using namespace std;

int nxt[N], n, m, ans = 0;
char a[N], b[N];

void get_next() {
    nxt[1] = 0;
    for (int i = 2, j = 0; i <= m; i++) {
        while (j > 0 && b[i] != b[j + 1]) j = nxt[j];
        if (b[i] == b[j + 1])
            ++j;
        nxt[i] = j;
    }
    return;
}

void kmp() {
    for (int i = 1, j = 0; i <= n; i++) {
        while (j > 0 && (j == n || a[i] != b[j + 1])) j = nxt[j];
        if (a[i] == b[j + 1])
            ++j;
        if (j == m) {
            ++ans;
            j = 0;
        }
    }
    return;
}

int main() {
    while (1) {
        ans = 0;
        scanf("%s", a + 1);
        n = strlen(a + 1);
        if (a[1] == ‘#‘ && n == 1)
            break;
        scanf("%s", b + 1);
        m = strlen(b + 1);
        get_next();
        kmp();
        printf("%d\n", ans);
    }
    return 0;
}

第二题

#10045 「一本通 2.2 练习 1」Radio Transmission

最小循环节会不会?不会。请自行百度。

既然大家都会最小循环节,那这就是水题啦。

原本答案为 (n-next[n])%n==0?n-next[n]:0 现在为 n-next[n]。(想一想为什么)

#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iostream>
#define N 1000010
using namespace std;

int nxt[N], n;
char s[N];

int main() {
    scanf("%d", &n);
    scanf("%s", s + 1);
    nxt[1] = 0;
    for (int i = 2, j = 0; i <= n; i++) {
        while (j > 0 && s[i] != s[j + 1]) j = nxt[j];
        if (s[i] == s[j + 1])
            ++j;
        nxt[i] = j;
    }
    printf("%d\n", n - nxt[n]);
    return 0;
}

第三题

警告:下面三道题都是神仙题(但是因为我竟然会做,所以就是水题啦),萌新慎入。

#10046 「一本通 2.2 练习 2」OKR-Periods of Words

首先,你要读懂题...。

题目要求的是求出最大周期长度之和

\(A\)\(B\) 的最大周期长度,当且仅当 \(A\)\(B\) 的前缀(\(0<|A|<|B|\)),且 \(B\) 为多个 \(A\) 组成的字符串的前缀。

然后经过简单的分析:

最大周期长度 = 总长度 - 最小前缀后缀

自证不难。

那么问题就变为了求最小前缀后缀

通过 KMP,我们会 \(O(N)\) 求最大前缀后缀,那么最小前缀后缀其实就是 \(\underbrace{next[next[next[...next[i]]]]}_{k个next}\)

就这么递归下去,最坏时间复杂度为:\(O(N^2)\)

所以...,肯定是不行的。

别慌,剖析每一次的递归,发现除了最后一个 \(next[]\),另外 \(k-1\) 个早就计算过了,所以记录下来就可以啦。

然后就变成了 \(O(N)\) 的复杂度了。

#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iostream>
#define N 1000010
using namespace std;

int nxt[N], cnt[N], n;
char s[N];

int main() {
    scanf("%d", &n);
    scanf("%s", s + 1);
    nxt[1] = 0;
    for (int i = 2, j = 0; i <= n; i++) {
        while (j > 0 && s[i] != s[j + 1]) j = nxt[j];
        if (s[i] == s[j + 1])
            ++j;
        nxt[i] = j;
    }
    long long ans = 0;
    for (int i = 1; i <= n; i++) {
        if (nxt[i] != 0)
            cnt[i] = cnt[nxt[i]];
        else
            cnt[i] = i;
        ans += i - cnt[i];
    }
    printf("%lld\n", ans);
    return 0;
}

第四题

#10047 「一本通 2.2 练习 3」似乎在梦中见过的样子

什么鬼名字

就是找一个形如 \(A+B+A\) 的字符串,要求 \(|A|\geq k,|B|\geq 1\)

其实他的前缀后缀就是 \(A\) 啦(很明显吧),然后就想到了 KMP。

但是注意 \(|B|\geq 1\),所以 \(|A|\times 2<|S|\)。(\(S\) 为原字符串)

然后...,就是大家最喜欢的暴力时间了,直接枚举串的左端点,KMP 时再枚举右端点。

\(O(N^2)\) 不会 T 吗?\(N\leq 1.5\times 10^4,N^2\leq 225,000,000=2.25\times 10^8\)。勉勉强强吧。(CCF 老爷机就算了)

#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iostream>
#define N 15010
using namespace std;

int k, nxt[N], n;
char s[N];

int kmp(char* a) {
    nxt[0] = nxt[1] = 0;
    int len = strlen(a + 1);
    int sum = 0, f;
    for (int i = 2, j = 0; i <= len; i++) {
        while (j > 0 && a[i] != a[j + 1]) j = nxt[j];
        if (a[i] == a[j + 1])
            ++j;
        f = nxt[i] = j;
        while (f * 2 >= i) f = nxt[f];
        if (f >= k)
            ++sum;
    }
    return sum;
}

int main() {
    scanf("%s", s + 1);
    scanf("%d", &k);
    n = strlen(s + 1);
    int ans = 0;
    for (int i = 0; i < n; i++) {
        ans += kmp(s + i);
    }
    printf("%d\n", ans);
    return 0;
}

第五题

#10048 「一本通 2.2 练习 4」Censoring

KMP 的题目怎么能用 KMP 做呢

发现这同样是一道删除类的题目,但是要求输出删除后的结果。[麻烦.jpg]

我们发现,删掉一段 T 之后,被删除的部分前面的一段可能和后面的一段连接起来出现新的 T 。

所以我们删掉一段 T 之后应该接着被删除的位置之前的继续向后匹配。

那么我们维护一个栈,匹配时如果栈顶出现了 T ,就弹出 T 个字符,然后继续从新的栈顶向后匹配就可以了。

匹配使用 KMP 算法。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <cmath>
 
using namespace std;
 
const int MaxL = 1000000 + 5;
 
int l, lt, Top;
int Next[MaxL], f[MaxL];
 
char S[MaxL], T[MaxL], Str[MaxL];
 
void Prepare()
{
    int j = Next[1] = 0;
    for (int i = 2; i <= lt; ++i)
    {
        while (j > 0 && T[j + 1] != T[i]) j = Next[j];
        if (T[j + 1] == T[i]) ++j;
        Next[i] = j;
    }
}
 
int main()
{
    scanf("%s%s", S + 1, T + 1);
    l = strlen(S + 1); lt = strlen(T + 1);
    Prepare();
    int j = 0;
    for (int i = 1; i <= l; ++i)
    {
        Str[++Top] = S[i];
        while (j > 0 && T[j + 1] != Str[Top]) j = Next[j];
        if (T[j + 1] == Str[Top]) ++j;
        if (j == lt)
        {
            Top -= lt;
            j = f[Top];
        }
        else f[Top] = j;
    }
    Str[Top + 1] = 0;
    printf("%s\n", Str + 1);
    return 0;
}

以上内容转载自这篇文章

然后是乱搞方法:

字符串 Hash,同样维护一个栈,但是用字符串哈希来比对。

优点:

  1. 码量小很多,思维难度低。

缺点:

  1. 时间看似一样但是不知道为什么常数很大,导致时间比 KMP 慢了一些。
  2. 空间消耗大。

但是在能过的基础上,字符串哈希肯定是首选。

#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iostream>
#define N 1000010
#define base 13331ULL
using namespace std;
typedef unsigned long long ull;

int n, m, tot = 0;
ull p[N], f[N], ha, jl;
char a[N], b[N], ans[N];

int main() {
    scanf("%s", a + 1);
    scanf("%s", b + 1);
    n = strlen(a + 1);
    m = strlen(b + 1);
    p[0] = 1;
    for (int i = 1; i <= m; i++) {
        p[i] = p[i - 1] * base;
        ha = ha * base + b[i] - ‘a‘ + 1;
    }
    for (int i = 1; i <= n; i++) {
        ans[++tot] = a[i];
        f[tot] = f[tot - 1] * base + a[i] - ‘a‘ + 1;
        if (tot - m < 0)
            continue;
        if (f[tot] - f[tot - m] * p[m] == ha)
            tot -= m;
    }
    for (int i = 1; i <= tot; i++) printf("%c", ans[i]);
    printf("\n");
    return 0;
}

KMP刷题记录

原文:https://www.cnblogs.com/lpf-666/p/12667996.html

(1)
(1)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!