Luogu 1439 最长公共子序列 O(NlogN)做法

对于一般的最长公共子序列问题(LCS),我们有O(N ^ 2)的DP做法 易得状态转移方程为

dp[i][j] = \left\{\begin{matrix} dp[i - 1][j - 1] + 1 (S1[i] == S2[j]) & & \\ max(dp[i-1][j], dp[i][j - 1]) & & \end{matrix}\right.

但是对于Luogu 1439 https://www.luogu.org/problemnew/show/P1439

N=100000的数据范围而言,这种做法无论在时间上还是空间上都是无法通过的

但LCS问题有一个特殊点,即S1与S2都是一段相同数字集合的不同排列

而对于100000这个数字 经验丰富的ACMer/OIer都会敏锐地察觉到需要用到一个O(NlogN)的做法

在动态规划中 什么问题有O(NlogN)的做法呢?

没错,就是最长上升子序列问题(LIS)。

那么我们显然需要一种方法,将LCS问题转化为LIS问题。

对于样例

5

3 2 1 4 5

1 2 3 4 5

进行研究

我们发现我们第二个数字序列中的每一个数字在第一个序列中都能找到其位置(反之亦然)

我们将这个位置表示为pos[i],那么就有

pos[i] = 3, 2, 1, 4, 5

我们会很显然的发现 LCS的答案和pos数组的LIS 竟然完全相同

 为什么会这样?

首先,由于我们要找的是LCS,那么这段子序列在其中一个序列中的顺序必然是上升的

建立一个pos数组,实际上是利用了hash的思想

换言之我们找的并不是一个具体的数字序列,而是抽象的顺序序列

只要第二个序列中顺序上升的序列hash到第一个序列中顺序仍保持上升,那么显然这段序列至少是一段公共序列的一段子集

遍历完整个序列之后,显然会得到最长的那个序列即我们求的LCS。

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int MAXN = 100000 + 10;

int N;
int S1[MAXN], S2[MAXN];
int hash[MAXN], pos[MAXN], dp[MAXN]; 

inline int read() {
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') f = -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); }
	return x * f;
}

int main() {
	N = read(); 
	memset(dp, 0x3f3f3f3f, sizeof(dp));
	for(int i = 1; i <= N; ++i) S1[i] = read(), hash[S1[i]] = i;
	for(int i = 1; i <= N; ++i) S2[i] = read(), pos[i] = hash[S2[i]];
	for(int i = 1; i <= N; ++i) *lower_bound(dp + 1, dp + N + 1, pos[i]) = pos[i];
	printf("%d\n", lower_bound(dp + 1, dp + N + 1, 0x3f3f3f3f) - dp - 1);
	return 0;
} 

这样我们就在O(NlogN)内解决了LCS问题。

猜你喜欢

转载自blog.csdn.net/LZUACMer/article/details/83350834