Secretary、Long Long Message
题目大概意思:
给出两个字符串 ,求 与 的最大公共子串的长度。其中 的长度均不超过 .
样例输入
yeshowmuchiloveyoumydearmotherreallyicannotbelieveit
yeaphowmuchiloveyoumydearmother
样例输出
27
分析:
首先考虑朴素的方法,枚举 的所有可能的子串,与 的所有可能的子串判断是否相等,并记录相等的最长的一对,子串共有 条,每次判断相等的时间复杂度为 ,故这样的算法的时间复杂度为 . 考虑到长度不同的两条子串一定不相等,因此如果 每条子串只与长度相同的 的子串判断是否相等,则时间复杂度降低为 . 这是不借助任何数据结构或高效算法所得到的低效解法。
接下来我们考虑如何进一步降低复杂度。假定 与 的最长公共子串的长度为 ,那么 与 一定也存在长度小于 的公共子串,相反的,一定不存在长度大于 的公共子串。因此,如果对于一个假定的长度 ,能够判断出 与 是否存在长度为 的公共子串,那么就不需要再枚举 的所有长度的子串了。通过对 的值进行二分搜索,只需对 个长度进行可行性判断即可。对于一个确定的长度,共有 条 的子串可能是 的子串,而每一条子串需要与 条 的等长的子串判断是否相等,每次判断的时间复杂度为 . 因此通过对最长公共子串的长度进行二分搜索,时间复杂度降低为了 .
对于 的某一长度为 的子串 ,当 与 的所有长度同为 的子串逐一比较是否相同时, 中的每一字符被访问了 次,同样的, 的每一条长度为 的子串,会与 的 条长度为 的子串比较,导致 中的每一条长度为 的子串的每一字符被访问了 次。由于我们只是在判断字符串是否相同,而无需比较字典序,因此如果我们能预先处理出 与 的每一条长度为 的子串的哈希值,那么在判断两字符串是否相同时,只需在 的时间复杂度内比较两字符串的哈希值即可。可是长度为 的子串有 条,每一条的长度为 ,如果逐一计算哈希值,每个字符依然会被访问 次,预处理的时间复杂度依然高达 . 因此我们可以使用滚动哈希的算法,该算法只依次访问每个元素 次,能够在 的时间复杂度内计算出所有长度为 的子串的哈希值,于是时间复杂度降低为了 .
可是字符串的长度高达 ,还是无法在时间限制内求解。我们接着考虑,在假定了一个长度 并判断是否可行时, 与 的长度为 的子串的哈希值的数量均为 . 对于 的每一个长度为 的子串的哈希值,只需判断 中是否存在相等的哈希值即可。而这可以使用平衡二叉查找树这一数据结构高效地完成。首先在 的时间复杂度内逐一将 的所有长为 的子串的哈希值插入,再对 的长尾 的子串的哈希值逐一查找,每次查找的时间复杂度是 ,因此可以在 的时间复杂度内判断出 与 是否存在长度为 的公共子串。由于这里只需要静态地查找,因此也可以先对 的所有长度为 的子串的哈希值排序,再用二分法逐一查找,时间复杂度同样是 ,如果我们对这些哈希值再次进行哈希,则可以在 的期望时间复杂度内完成一次查找,时间复杂度还可以进一步降低至 .
这样,我们已经可以在不超过 的时间复杂度内求出 与 的最长公共子串了,足以在时间限制完成。
像这样从复杂度较高的算法出发,不断降低复杂度直到满足问题要求的过程,是设计算法时常会经历的过程。
下面贴代码:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
const ull B = 54788567;
const int MAX_N = 100005;
char X[MAX_N], Y[MAX_N];
ull St[MAX_N];
int max_substr(const char* a, const char* b);
int main()
{
scanf("\n%s\n%s", X, Y);
printf("%d\n", max_substr(X, Y));
return 0;
}
int max_substr(const char* a, const char* b)
{
int al = strlen(a), bl = strlen(b);
if (al > bl)
{
swap(a, b);
swap(al, bl);
}
int lb = 0, rb = al + 1;
while (lb + 1 < rb)
{
int mid = (lb + rb) >> 1;
bool C = false;
ull t = 1, ah = 0, bh = 0;
for (int i = 0; i < mid; ++i)
{
t *= B;
ah = ah * B + a[i];
bh = bh * B + b[i];
}
int cnt = 0;
St[cnt++] = bh;
for (int i = mid; i < bl; ++i)
{
bh = bh * B + b[i] - b[i - mid] * t;
St[cnt++] = bh;
}
sort(St, St + cnt);
if (*lower_bound(St, St + cnt, ah) == ah)
{
C = true;
}
else for (int i = mid; i < al; ++i)
{
ah = ah * B + a[i] - a[i - mid] * t;
if (*lower_bound(St, St + cnt, ah) == ah)
{
C = true;
break;
}
}
if (C)
{
lb = mid;
}
else
{
rb = mid;
}
}
return lb;
}
另外,利用后缀数组和高度数组,同样可以高效地求解本问题:
首先来考虑一个简化的问题:计算一个字符串中至少出现两次的最长子串。答案一定会在后缀数组中相邻的两个后缀的公共前缀之中,所以只要考虑它们就好了。这是因为子串的开始位置在后缀数组中相距越远,其公共前缀的长度也就越短。因此,高度数组的最大值就是答案。
再来考虑原问题的解法。因为对于两个字符串,不好直接运用后缀数组,所以我们可以把 和 ,通过在中间插入一个不会出现的字符(例如 ‘$’)拼成一个字符串 . 然后计算 的后缀数组,检查后缀数组中的所有相邻后缀。其中,所有分属于 和 的不同字符串的后缀的最大公共前缀长度的最大值即为答案。而要知道后缀是属于 还是 ,可以由其在 中的位置直接判断。
下面贴代码:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAX_N = 200005;
int rnk[MAX_N];
int tmp[MAX_N];
int lens, k;
int sa[MAX_N]; // 后缀数组
int lcp[MAX_N];// 高度数组
bool compare_sa(const int& i, const int& j);
void construct_sa(const char* const S, const int N, int* const sa);
void construct_lcp(const char* const S, const int N, const int* const sa, int* const lcp);
char A[MAX_N];
int main()
{
scanf("\n%[^\n]", A);
int L = strlen(A);
scanf("\n%[^\n]", A + L + 1);
A[L] = '$';// 插入一个不会出现的字符
construct_sa(A, L + strlen(A + L), sa);
construct_lcp(A, lens, sa, lcp);
int ans = 0;
for (int i = 0; i < lens; ++i)
{
if ((sa[i] < L) ^ (sa[i + 1] < L))
{
ans = max(ans, lcp[i]);
}
}
printf("%d\n", ans);
return 0;
}
bool compare_sa(const int& i, const int& j)
{
if (rnk[i] != rnk[j])
{
return rnk[i] < rnk[j];
}
else
{
int ri = i + k <= lens ? rnk[i + k] : -1;
int rj = j + k <= lens ? rnk[j + k] : -1;
return ri < rj;
}
}
// 倍增法计算后缀数组
void construct_sa(const char* const S, const int N, int* const sa)
{
lens = N;
for (int i = 0; i <= lens; ++i)
{
sa[i] = i;
rnk[i] = i < lens ? S[i] : -1;
}
for (k = 1; k <= lens; k <<= 1)
{
sort(sa, sa + lens + 1, compare_sa);
tmp[sa[0]] = 0;
for (int i = 0; i <= lens; ++i)
{
tmp[sa[i]] = tmp[sa[i - 1]] + (compare_sa(sa[i - 1], sa[i]) ? 1 : 0);
}
memcpy(rnk, tmp, (lens + 1) * sizeof(int));
}
}
// 计算高度数组
void construct_lcp(const char* const S, const int N, const int* const sa, int* const lcp)
{
for (int i = 0; i <= N; ++i)
{
rnk[sa[i]] = i;
}
int h = 0;
lcp[0] = 0;
for (int i = 0; i < N; ++i)
{
int j = sa[rnk[i] - 1];
if (h)
{
--h;
}
for (; j + h < N && i + h < N; ++h)
{
if (S[j + h] != S[i + h])
{
break;
}
}
lcp[rnk[i] - 1] = h;
}
}