J-HDU 2594 (KMP & 扩展KMP)

HDU 2594 Simpsons’ Hidden Talents
为了搞定这题,先要学习扩展KMP算法。当然这题用普通KMP求next数组也可以做,两种方法的AC代码都会放在最后。

扩展KMP

扩展 KMP 解决的是这样的问题:
给定母串 S 和子串 T,要求母串每个后缀与子串的最长公共前缀。
例如,设母串 S = “marjoriz”,T = “riemann”,取母串长度为 3 的后缀 “riz”,则该后缀与 T 的最长公共前缀为 “ri”,长度为2。

在正式算法之前,先了解扩展 KMP 依赖的数据结构。
扩展 KMP 定义了两个数组,分别是 next[] 数组(简写为nxt[])、extend[]数组(简写为ex[]),这里的 nxt[] 数组与普通 KMP 算法的 next[] 数组定义不一样。
nxt[j]:代表子串 T 以 j 为起点的后缀和其自身 T 的最长公共前缀长度;
ex[i]:代表母串 S 以 i 为起点的后缀和子串 T 的最长公共前缀长度。
可以发现 nxt[] 和 ex[] 的定义很相似,只要把 T 自己既当作母串又当作子串,那么 nxt[] 的定义就和 ex[] 一致了,因此先给出 nxt[] 的求法。

如下图所示,把 T 当作母串,把 T’ (其实也就是 T 自身)当作子串,假设现在来到下标为 i 的位置,0~i-1 的nxt[]数组已经求出,现想要求 nxt[i],也就是 T 以 i 为起点的后缀与 T’ 的最长公共前缀。
额外定义一个变量 po,代表 i 左边以该点为起点的后缀与 T’ 最长公共前缀值最大的那个点,即 nxt[po] 最大的那个点。
那么根据 nxt[] 的定义,图中的绿色部分是完全相同的。
因此,

  1. T 中的 i ~ po + nxt[po] 也与 T’ 中的 i - po ~ nxt[po] 完全相同;
  2. 而 nxt[i - po] 已经求出,根据 nxt[] 的定义,图中 T’ 的红色半透明部分也完全相同;

根据 1. 和 2. 可知,T[i ~ i + nxt[i - po]] 应与 T’ 的两处红色半透明部分也相同。

那么红色部分的长度有两种情况:
一、红色部分的长度没有超过 po + nxt[po] 的位置
二、红色部分的长度超过了 po + nxt[po] 的位置
求nxt[]数组之一
先讨论第一种情况,红色部分的长度没有超过 po + nxt[po] 的位置。
还是如上图所示,T 以 i 为起点的后缀与 T’ 的最长公共前缀就恰好是 nxt[i - po] (图中红色部分),也就是说 nxt[i] = nxt[i - po] 可以直接求出。

再讨论第二种情况,红色部分的长度超过了 po + nxt[po] 的位置,也就是 i + nxt[i - po] > po + nxt[po] 如下图所示。
求nxt[]数组之二
假设 j = (po + nxt[po]) - i,也就是 T 中大括号括起来的长度,那么根据绿色部分相同有:T[i ~ i + j] = T’[0 ~ j],这两个部分是完全相同的,无需再进行比较,而只需要从 T[i + j] 和 T’[j] 开始往后比较即可。
逐个比较直到失配,算出 nxt[i] 的值后,由于以 i 开头的后缀的最远匹配位置超过了以 po 开头的后缀的最远匹配位置,因此还需要更新 po = i。
至此,两种情况讨论完毕,让 i 从左到右遍历即可求出子串 T 的整个 nxt[] 数组。


那么回到正题,如何求母串 S 的每个后缀与 T 的最长公共前缀呢?方法完全类似,同样额外定义一个 po 变量,同样分为超过 po + ex[po] 与未超过两种情况。
回顾一下 ex[] 数组的定义:ex[i] 代表母串 S 以 i 为起点的后缀与子串 T 的最长公共前缀。
所以,

  1. 图中母串 S[po ~ po + ex[po]] 与 子串 T[0 ~ ex[po]] (图中绿色部分)完全相同。
  2. 并且根据 nxt[] 的定义,子串 T 的两处红色部分完全相同。

根据 1. 和 2. 可知,母串 S 的红色部分与 子串 T 最左边的红色部分完全相同。
求ex[]数组之一
情况一、红色部分的长度没有超过 po + ex[po] 的位置,如上图所示。
显然 ex[i] = nxt[i - po] (即红色部分的长度)。

情况二、红色部分的长度超过了 po + ex[po] 的位置,如下图所示。
求ex[]数组之二
易知 S[i ~ i + j] 与 T[0 ~ j] 完全相同,不用再比较,只需要从 S[i + j] 和 T[j] 继续往后比较即可求出 ex[i]。同样地,失配后记得更新 po = i。


时间复杂度分析:
设母串 S 长度为 N,子串 T 长度为 M,由于母串的每个位置只访问一次,因此求 ex[] 的时间复杂度为 O(N)。同理,求 nxt[] 的时间复杂度为 O(M),扩展 KMP 的总时间复杂度为 O(M + N)。

AC代码

方法一. 合并 s1 和 s2,求合并串的 nxt[] 数组

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace::std;
const int maxn = 50010;

char s1[2 * maxn];
char s2[maxn];
int nxt[2 * maxn];
void getnxt(int len) {
    memset(nxt, -1, sizeof(nxt));
    int j = 0, k = -1;
    while (j < len) {
        if (k == -1 || s1[j] == s1[k]) {
            j++, k++;
            nxt[j] = k;
        } else k = nxt[k];
    }
}
void solve(void) {
    int l1 = strlen(s1), l2 = strlen(s2);
    strcat(s1, s2); // 合并s1和s2再求该合并串的next[]数组即可
    int l12 = strlen(s1);
    getnxt(l12);
    int k = nxt[l12];
    if (k == 0) {
        printf("0\n");
        return;
    }
    while (k > l1 || k > l2) k = nxt[k]; // 长度不能超过l1和l2
    s1[k] = '\0';
    printf("%s %d\n", s1, k);
}

int main(void) {
    while (scanf("%s", s1) != EOF) {
        scanf("%s", s2);
        solve();
    }
    return 0;
}

方法二. 用扩展 KMP 求母串的每个后缀与子串的最长公共前缀
根据题意,s2 必须是整个后缀都与 s1 的前缀匹配,并不允许部分匹配,所以要筛选出 ex[] 数组中满足:ex[k] + k == strlen(s2) 的最大的那个值。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace::std;
const int maxn = 50010;

char s1[maxn]; // 子串
char s2[maxn]; // 母串
int nxt[maxn]; // 扩展kmp专用, 子串后缀[j~m-1]与子串前缀的最长公共长度
int ex[maxn]; // 扩展kmp专用, 母串[i~n-1]与子串前缀的最长公共长度
// 扩展kmp算法, 求母串S的每一个后缀与子串T的最长公共前缀
void getnxt(void) {
    memset(nxt, 0, sizeof(nxt));
    int l1 = strlen(s1);
    int i = 0;
    nxt[0] = l1; // nxt[0]=子串长度
    while (i + 1 < l1 && s1[i] == s1[i + 1]) i++; // 计算nxt[1]
    nxt[1] = i;
    int po = 1; // 前面匹配达到的最远位置
    for (i = 2; i < l1; ++i) {
        if (nxt[i - po] + i < nxt[po] + po) // 情况1:可以直接得到nxt[i]
            nxt[i] = nxt[i - po];
        else { // 情况2:要继续往后匹配才能得到nxt[i]
            int j = nxt[po] + po - i;
            if (j < 0) j = 0; // 从头开始匹配
            while (i + j < l1 && s1[j] == s1[i + j]) j++; // 继续往后
            nxt[i] = j;
            po = i; // 更新po的位置
        }
    }
}
void exkmp(void) {
    int l1 = strlen(s1), l2 = strlen(s2);
    getnxt();
    memset(ex, 0, sizeof(ex));
    int i = 0, po = 0;
    while (s2[i] == s1[i] && i < l1 && i < l2) i++; // 计算nx[0]
    ex[0] = i;
    for (i = 1; i < l2; i++) {
        if (nxt[i - po] + i < ex[po] + po) // 情况1:可以直接得到ex[i]
            ex[i] = nxt[i - po];
        else {
            int j = ex[po] + po - i; // 子串开始匹配的位置
            if (j < 0) j = 0; // 从头开始匹配
            while (i + j < l2 && j < l1 && s1[j] == s2[j + i]) j++; // 继续往后
            ex[i] = j;
            po = i; // 更新po的位置
        }
    }
}

int main(void) {
    while (scanf("%s", s1) != EOF) {
        scanf("%s", s2);
        exkmp();
        int l2 = strlen(s2);
        int max_val = 0, max_index = 0;
        for (int i = 0; i < l2; ++i) { // 找到最大的匹配长度
            if (ex[i] > max_val && ex[i] + i == l2) { // 母串后缀的匹配长度必须到底
                max_val = ex[i];
                max_index = i;
            }
        }
        if (max_val) printf("%s %d\n", s2 + max_index, max_val);
        else printf("0\n");
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值