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;
}
}