POJ2217 Secretary、POJ2774 Long Long Message - 最長公共子串 - 字符串哈希+二分/後綴數組

SecretaryLong Long Message

 
 

題目大概意思:

給出兩個字符串 S,TS,T ,求 SSTT 的最大公共子串的長度。其中 S,TS,T 的長度均不超過 10510^5 .

 

樣例輸入

yeshowmuchiloveyoumydearmotherreallyicannotbelieveit
yeaphowmuchiloveyoumydearmother

 

樣例輸出

27

 
 

分析:

首先考慮樸素的方法,枚舉 SS 的所有可能的子串,與 TT 的所有可能的子串判斷是否相等,並記錄相等的最長的一對,子串共有 O(n2)O(n^2) 條,每次判斷相等的時間複雜度爲 O(n)O(n) ,故這樣的算法的時間複雜度爲 O(n5)O(n^5) . 考慮到長度不同的兩條子串一定不相等,因此如果 SS 每條子串只與長度相同的 TT 的子串判斷是否相等,則時間複雜度降低爲 O(n4)O(n^4) . 這是不借助任何數據結構或高效算法所得到的低效解法。

接下來我們考慮如何進一步降低複雜度。假定 SSTT 的最長公共子串的長度爲 xx ,那麼 SSTT 一定也存在長度小於 xx 的公共子串,相反的,一定不存在長度大於 xx 的公共子串。因此,如果對於一個假定的長度 xx ,能夠判斷出 SSTT 是否存在長度爲 xx 的公共子串,那麼就不需要再枚舉 SS 的所有長度的子串了。通過對 xx 的值進行二分搜索,只需對 O(log2n)O(\log_2n) 個長度進行可行性判斷即可。對於一個確定的長度,共有 O(n)O(n)SS 的子串可能是 TT 的子串,而每一條子串需要與 O(n)O(n)TT 的等長的子串判斷是否相等,每次判斷的時間複雜度爲 O(n)O(n) . 因此通過對最長公共子串的長度進行二分搜索,時間複雜度降低爲了 O(n3log2n)O(n^3·\log_2n) .

對於 SS 的某一長度爲 ll 的子串 uu ,當 uuTT 的所有長度同爲 ll 的子串逐一比較是否相同時, uu 中的每一字符被訪問了 O(n)O(n) 次,同樣的,TT 的每一條長度爲 ll 的子串,會與 SSO(n)O(n) 條長度爲 ll 的子串比較,導致 TT 中的每一條長度爲 ll 的子串的每一字符被訪問了 O(n)O(n) 次。由於我們只是在判斷字符串是否相同,而無需比較字典序,因此如果我們能預先處理出 SSTT 的每一條長度爲 ll 的子串的哈希值,那麼在判斷兩字符串是否相同時,只需在 O(1)O(1) 的時間複雜度內比較兩字符串的哈希值即可。可是長度爲 ll 的子串有 O(n)O(n) 條,每一條的長度爲 O(n)O(n) ,如果逐一計算哈希值,每個字符依然會被訪問 O(n)O(n) 次,預處理的時間複雜度依然高達 O(n2)O(n^2) . 因此我們可以使用滾動哈希的算法,該算法只依次訪問每個元素 O(1)O(1) 次,能夠在 O(n)O(n) 的時間複雜度內計算出所有長度爲 ll 的子串的哈希值,於是時間複雜度降低爲了 O(n2log2n)O(n^2·\log_2n) .

可是字符串的長度高達 10510^5 ,還是無法在時間限制內求解。我們接着考慮,在假定了一個長度 ll 並判斷是否可行時, SSTT 的長度爲 ll 的子串的哈希值的數量均爲 O(n)O(n) . 對於 SS 的每一個長度爲 ll 的子串的哈希值,只需判斷 TT 中是否存在相等的哈希值即可。而這可以使用平衡二叉查找樹這一數據結構高效地完成。首先在 O(nlog2n)O(n·\log_2n) 的時間複雜度內逐一將 TT 的所有長爲 ll 的子串的哈希值插入,再對 SS 的長尾 ll 的子串的哈希值逐一查找,每次查找的時間複雜度是 O(log2n)O(\log_2n) ,因此可以在 O(nlog2n)O(n·\log_2n) 的時間複雜度內判斷出 SSTT 是否存在長度爲 ll 的公共子串。由於這裏只需要靜態地查找,因此也可以先對 TT 的所有長度爲 ll 的子串的哈希值排序,再用二分法逐一查找,時間複雜度同樣是 O(nlog2n)O(n·log_2n) ,如果我們對這些哈希值再次進行哈希,則可以在 O(1)O(1) 的期望時間複雜度內完成一次查找,時間複雜度還可以進一步降低至 O(n)O(n) .

這樣,我們已經可以在不超過 O(nlog22n)O(n·\log_2^2n) 的時間複雜度內求出 SSTT 的最長公共子串了,足以在時間限制完成。

像這樣從複雜度較高的算法出發,不斷降低複雜度直到滿足問題要求的過程,是設計算法時常會經歷的過程。

 
 
下面貼代碼:

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

 
 

另外,利用後綴數組和高度數組,同樣可以高效地求解本問題:

首先來考慮一個簡化的問題:計算一個字符串中至少出現兩次的最長子串。答案一定會在後綴數組中相鄰的兩個後綴的公共前綴之中,所以只要考慮它們就好了。這是因爲子串的開始位置在後綴數組中相距越遠,其公共前綴的長度也就越短。因此,高度數組的最大值就是答案。

再來考慮原問題的解法。因爲對於兩個字符串,不好直接運用後綴數組,所以我們可以把 SSTT ,通過在中間插入一個不會出現的字符(例如 ‘$’)拼成一個字符串 SS&#x27; . 然後計算 SS&#x27; 的後綴數組,檢查後綴數組中的所有相鄰後綴。其中,所有分屬於 SSTT 的不同字符串的後綴的最大公共前綴長度的最大值即爲答案。而要知道後綴是屬於 SS 還是 TT ,可以由其在 SS&#x27; 中的位置直接判斷。

 
 
下面貼代碼:

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章