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[] 的定义,图中的绿色部分是完全相同的。
因此,
- T 中的 i ~ po + nxt[po] 也与 T’ 中的 i - po ~ nxt[po] 完全相同;
- 而 nxt[i - po] 已经求出,根据 nxt[] 的定义,图中 T’ 的红色半透明部分也完全相同;
根据 1. 和 2. 可知,T[i ~ i + nxt[i - po]] 应与 T’ 的两处红色半透明部分也相同。
那么红色部分的长度有两种情况:
一、红色部分的长度没有超过 po + nxt[po] 的位置
二、红色部分的长度超过了 po + nxt[po] 的位置
先讨论第一种情况,红色部分的长度没有超过 po + nxt[po] 的位置。
还是如上图所示,T 以 i 为起点的后缀与 T’ 的最长公共前缀就恰好是 nxt[i - po] (图中红色部分),也就是说 nxt[i] = nxt[i - po] 可以直接求出。
再讨论第二种情况,红色部分的长度超过了 po + nxt[po] 的位置,也就是 i + nxt[i - po] > po + nxt[po] 如下图所示。
假设 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 的最长公共前缀。
所以,
- 图中母串 S[po ~ po + ex[po]] 与 子串 T[0 ~ ex[po]] (图中绿色部分)完全相同。
- 并且根据 nxt[] 的定义,子串 T 的两处红色部分完全相同。
根据 1. 和 2. 可知,母串 S 的红色部分与 子串 T 最左边的红色部分完全相同。
情况一、红色部分的长度没有超过 po + ex[po] 的位置,如上图所示。
显然 ex[i] = nxt[i - po] (即红色部分的长度)。
情况二、红色部分的长度超过了 po + ex[po] 的位置,如下图所示。
易知 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;
}