最長公共子序列

归游發表於2024-11-03

P1439 【模板】最長公共子序列

DP的經典例題,適合學完導彈攔截後再來學習

O(\(N^2\))

按照DP常規思考方法

我們令dp[i][j]為P1序列前i個子序列和P2序列前j個子序列的最長公共子序列長度

  • 注意,這是一種常見的設dp狀態的方式,可以積累)

所以我們進而思考狀態轉移方程

對於\(dp[i][j]\) 應該和\(dp[i-1][j],dp[i][j-1],dp[i-1][j-1]\)有關

  • 無論\(p1[i]\)是否等於\(p2[j],dp[i][j]=dp[i-1][j]\),同理,\(dp[i][j]=dp[i][j-1]\)
    所以\(dp[i][j]=max(dp[i-1][j],dp[i][j-1])\)
  • 而如果\(p1[i]==p2[j]\) 那麼最前面的公共子序列長度即可以+1,所以\(dp[i][j]=dp[i-1][j-1]+1\)
  • 而當我們狀態轉移,狀態從(i-1,j),(i-1,j-1),(i,j-1)這個三個轉移過來,這個又從(i-2,j),(i-2,j-2),(i-1,j-2),(i-2,j-1),(i-2,j-2),(i-1,j-1),(i-1,j-2),(i,j-2)轉移,以此類推,所以在狀態轉移到(i,j)之前,從(1,1)到(i-1,j)都有值,所以才有程式碼中的迴圈

綜上
$dp[i][j]=max(dp[i-1][j],dp[i][j-1]) $

\(if(p1[i]==p2[j]) dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1)\)

#include<iostream>

using namespace std;
const int maxn=1e4+10;
int n;
int a[maxn],b[maxn];
int f[maxn][maxn];
int main(){
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=1;i<=n;++i) cin>>b[i];
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j){//狀態轉移前都有值
			f[i][j]=max(f[i-1][j],f[i][j-1]);
			if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1); 
		} 
	cout<<f[n][n];
	return 0;
} 

O(\(nlogn\))

在第一種的處理方法中,我們忽略了題目條件中的一個性質:全排列

而此題與導彈攔截有類似之處,我們思考是否有可能將此題轉化為最長不下降子序列

在最長不下降子序列中,從一個序列中找一個最長不下降子序列
而此題中是找兩個最長公共子序列,所以我們找出的子序列即為另一個序列子序列,那就能解決該問題

(核心思路)

如果我們保證P1序列是上升序列,而從P2序列找到一個最長不下降子序列一定是P1序列的子序列,那麼該序列長度即為所求

重新編號就能保證P1序列是上升序列

  • 舉例
    P1:3 2 1 4 5
    P2:1 2 3 4 5
    重新編號(全排列保證了這樣重新編號的合理性)
    P1: 1 2 3 4 5
    P2: 3 2 1 4 5
  1. 重新編號也很簡單,有map對映即可
  2. 求最長上升子序列用導彈攔截的方法即可

CODE

#include<bits/stdc++.h>
using namespace std;
map<int,int>m;
int n;
const int maxn=1e5+10;
int a[maxn];
int f[maxn];
int main(){
	cin>>n;
	for(int i=1;i<=n;++i){
		int x;
		cin>>x;
		m[x]=i;
	}
	int t=0;
	for(int i=1;i<=n;++i){
		int x,l=0,r=t;
		cin>>x;
		x=m[x];
		while(l<=r){
			int mid=(l+r)>>1;
			if(x<f[mid]) r=mid-1;
			else l=mid+1;
		}
		if(l>t) t=l;
		f[l]=x; 
	}
	cout<<t<<endl;
	return 0; 
}

相關文章