POJ2217 Secretary、POJ2774 Long Long Message - 最长公共子串 - 字符串哈希+二分/后缀数组

SecretaryLong Long Message

 
 

题目大概意思:

给出两个字符串 S , T S,T ,求 S S T T 的最大公共子串的长度。其中 S , T S,T 的长度均不超过 1 0 5 10^5 .

样例输入

yeshowmuchiloveyoumydearmotherreallyicannotbelieveit
yeaphowmuchiloveyoumydearmother

样例输出

27

 
 

分析:

首先考虑朴素的方法,枚举 S S 的所有可能的子串,与 T T 的所有可能的子串判断是否相等,并记录相等的最长的一对,子串共有 O ( n 2 ) O(n^2) 条,每次判断相等的时间复杂度为 O ( n ) O(n) ,故这样的算法的时间复杂度为 O ( n 5 ) O(n^5) . 考虑到长度不同的两条子串一定不相等,因此如果 S S 每条子串只与长度相同的 T T 的子串判断是否相等,则时间复杂度降低为 O ( n 4 ) O(n^4) . 这是不借助任何数据结构或高效算法所得到的低效解法。

接下来我们考虑如何进一步降低复杂度。假定 S S T T 的最长公共子串的长度为 x x ,那么 S S T T 一定也存在长度小于 x x 的公共子串,相反的,一定不存在长度大于 x x 的公共子串。因此,如果对于一个假定的长度 x x ,能够判断出 S S T T 是否存在长度为 x x 的公共子串,那么就不需要再枚举 S S 的所有长度的子串了。通过对 x x 的值进行二分搜索,只需对 O ( log 2 n ) O(\log_2n) 个长度进行可行性判断即可。对于一个确定的长度,共有 O ( n ) O(n) S S 的子串可能是 T T 的子串,而每一条子串需要与 O ( n ) O(n) T T 的等长的子串判断是否相等,每次判断的时间复杂度为 O ( n ) O(n) . 因此通过对最长公共子串的长度进行二分搜索,时间复杂度降低为了 O ( n 3 log 2 n ) O(n^3·\log_2n) .

对于 S S 的某一长度为 l l 的子串 u u ,当 u u T T 的所有长度同为 l l 的子串逐一比较是否相同时, u u 中的每一字符被访问了 O ( n ) O(n) 次,同样的, T T 的每一条长度为 l l 的子串,会与 S S O ( n ) O(n) 条长度为 l l 的子串比较,导致 T T 中的每一条长度为 l l 的子串的每一字符被访问了 O ( n ) O(n) 次。由于我们只是在判断字符串是否相同,而无需比较字典序,因此如果我们能预先处理出 S S T T 的每一条长度为 l l 的子串的哈希值,那么在判断两字符串是否相同时,只需在 O ( 1 ) O(1) 的时间复杂度内比较两字符串的哈希值即可。可是长度为 l l 的子串有 O ( n ) O(n) 条,每一条的长度为 O ( n ) O(n) ,如果逐一计算哈希值,每个字符依然会被访问 O ( n ) O(n) 次,预处理的时间复杂度依然高达 O ( n 2 ) O(n^2) . 因此我们可以使用滚动哈希的算法,该算法只依次访问每个元素 O ( 1 ) O(1) 次,能够在 O ( n ) O(n) 的时间复杂度内计算出所有长度为 l l 的子串的哈希值,于是时间复杂度降低为了 O ( n 2 log 2 n ) O(n^2·\log_2n) .

可是字符串的长度高达 1 0 5 10^5 ,还是无法在时间限制内求解。我们接着考虑,在假定了一个长度 l l 并判断是否可行时, S S T T 的长度为 l l 的子串的哈希值的数量均为 O ( n ) O(n) . 对于 S S 的每一个长度为 l l 的子串的哈希值,只需判断 T T 中是否存在相等的哈希值即可。而这可以使用平衡二叉查找树这一数据结构高效地完成。首先在 O ( n log 2 n ) O(n·\log_2n) 的时间复杂度内逐一将 T T 的所有长为 l l 的子串的哈希值插入,再对 S S 的长尾 l l 的子串的哈希值逐一查找,每次查找的时间复杂度是 O ( log 2 n ) O(\log_2n) ,因此可以在 O ( n log 2 n ) O(n·\log_2n) 的时间复杂度内判断出 S S T T 是否存在长度为 l l 的公共子串。由于这里只需要静态地查找,因此也可以先对 T T 的所有长度为 l l 的子串的哈希值排序,再用二分法逐一查找,时间复杂度同样是 O ( n l o g 2 n ) O(n·log_2n) ,如果我们对这些哈希值再次进行哈希,则可以在 O ( 1 ) O(1) 的期望时间复杂度内完成一次查找,时间复杂度还可以进一步降低至 O ( n ) O(n) .

这样,我们已经可以在不超过 O ( n log 2 2 n ) O(n·\log_2^2n) 的时间复杂度内求出 S S T T 的最长公共子串了,足以在时间限制完成。

像这样从复杂度较高的算法出发,不断降低复杂度直到满足问题要求的过程,是设计算法时常会经历的过程。

 
 
下面贴代码:

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

 
 

另外,利用后缀数组和高度数组,同样可以高效地求解本问题:

首先来考虑一个简化的问题:计算一个字符串中至少出现两次的最长子串。答案一定会在后缀数组中相邻的两个后缀的公共前缀之中,所以只要考虑它们就好了。这是因为子串的开始位置在后缀数组中相距越远,其公共前缀的长度也就越短。因此,高度数组的最大值就是答案。

再来考虑原问题的解法。因为对于两个字符串,不好直接运用后缀数组,所以我们可以把 S S T T ,通过在中间插入一个不会出现的字符(例如 ‘$’)拼成一个字符串 S S&#x27; . 然后计算 S S&#x27; 的后缀数组,检查后缀数组中的所有相邻后缀。其中,所有分属于 S S T T 的不同字符串的后缀的最大公共前缀长度的最大值即为答案。而要知道后缀是属于 S S 还是 T T ,可以由其在 S S&#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;
	}
}

原创文章 42 获赞 22 访问量 3039

猜你喜欢

转载自blog.csdn.net/weixin_44327262/article/details/98360235
今日推荐