1. 程式人生 > >Luogu 1439 最長公共子序列 O(NlogN)做法

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.

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問題。