線性dp:最長公共子序列

Tomorrowland_D發表於2024-08-23

最長公共子序列

  • 本文講解的題與leetcode1143.最長公共子序列這題一樣,閱讀完可以挑戰一下。

力扣題目連結

題目敘述:

給定兩個字串,輸出其最長公共子序列,並輸出它的長度

輸入:

ADABEC和DBDCA

輸出:

DBC
3

解釋

最長公共子序列是DBC,其長度為3

動態規劃思路:

  • 我們這題先構建一個模型,我們使用兩個指標i,j ,分別用於遍歷a字串,b字串。如圖所示:

img

  • 然後我們可以設想一個狀態變數,也就是一個函式。一個關於兩個變數相關的函式,這在程式碼中體現為二維陣列f

  • 然後f[i][j]表示什麼呢?表示序列a[1,2,3....i]b[1,2,3....j]的最長公共子序列的長度

狀態變數的含義

  • 在這裡的狀態變數為f[i][j],它的含義是a的前i個字元與b的前j個字元的最長公共子序列的長度

  • 現在就要觀察a[i],b[j]是否在當前的最長公共子序列當中。

  • 具體情況如下圖:

img

遞推公式:

  • f[i][j]可以分為三種情況討論,就是:
  1. a[i]b[j]都在最長公共子序列當中,也就是a[i]==b[j]
  2. a[i]!=b[j],並且a[i]不在公共子序列當中。
  3. a[i]!=b[j],並且b[j]不在公共子序列當中。
  • 那我們的遞推公式就可與分為兩種情況:
    • f[i][j]=f[i-1][j-1]+1a[i]==b[j]
    • f[i][j]=max(f[i-1][j],f[i][j-1])a[i]!=b[j]
  • 顯而易見,我們的邊界條件為:
    • f[0][j]=0
    • f[i][0]=0
//m是a字串的長度,n是b字串的長度
for(int i=1;i<=m;i++){
    for(int j=1;j<=n;j++){
        //因為我們的f陣列是從下標1開始,而字串是從0開始的下標
        if(a[i-1]==b[j-1]) f[i][j]=f[i-1][j-1]+1;
        else f[i][j]=max(f[i-1][j],f[i][j-1]);
    }
}

遍歷順序

  • 經過上面的分析,明顯遍歷順序為i從小到大j也是從小到大

初始化

  • 初始化邊界為0即可

舉例列印dp陣列

  • 如圖所示

如何找出對應的最長公共子序列的長度

  • 我們使用p陣列來記錄每一次f[i][j]的值來源於哪一個方向

    • 1方向代表左上方
    • 2方向代表左方
    • 3方向代表上方
  • 程式碼改造如下:

for(int i=1;i<=m;i++){
    for(int j=1;j<=n;j++){
        if(a[i-1]==b[j-1]){
            f[i][j]=f[i-1][j-1]+1;
            //左上方
            p[i][j]=1;
        }
        else if(f[i-1][j]>f[i][j-1]){
            f[i][j]=f[i][j-1];
   			//左邊
            p[i][j]=2;
        }
        else{
            f[i][j]=f[i-1][j];
			//上邊
            p[i][j]=3;
        }
    }
}
  • p[i][j]代表前驅的位置。

演算法的執行過程

  • 我們要找到最長公共子序列,只需要找到從結尾開始,往前找到p[i][j]==1,也就是來源於左上方的哪些元素的集合,就是我們的最長公共子序列。(並不是棋盤中所有p[i][j]==1)的元素,而是從右下角出發,往回找到的所有p[i][j]==1的那些元素。
  • 例子如下:

img

  • 我們使用s陣列來儲存最長公共子序列

  • 程式碼實現:

int i,j,k;
char s[200];
i=m;j=n;k=f[m][n];
while(i>0&&j>0){
    //左上方
    if(p[i][j]==1){
        s[k--]=a[i-1];
        i--;j--;
    }
    //左邊
    else if(p[i][j]==2) j--;
    //上邊
    else i--;
}
for(int i=1;i<=f[m][n];i++) cout<<s[i];

最終程式碼實現:

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

char a[200];
char b[200];
int f[205][205];
int p[205][205];
int m, n;

void LCS() {
	int i, j;
	m = strlen(a);
	n = strlen(b);

	for (i = 1; i <= m; i++) {
		for (j = 1; j <= n; j++) {
			if (a[i - 1] == b[j - 1]) {
				f[i][j] = f[i - 1][j - 1] + 1;
				p[i][j] = 1; 
			}
			else if (f[i - 1][j] > f[i][j - 1]) {
				f[i][j] = f[i - 1][j];
				p[i][j] = 2; 
			}
			else {
				f[i][j] = f[i][j - 1];
				p[i][j] = 3; 
			}
		}
	}
	cout << f[m][n] << endl;
}
//尋找出當初的最長公共子序列。
void getLCS() {
	int i = m, j = n, k = f[m][n];
	char s[200];
	s[k] = '\0'; 
	while (i > 0 && j > 0) {
		if (p[i][j] == 1) {
			s[--k] = a[i - 1];
			i--; j--;
		}
		else if (p[i][j] == 2) {
			i--;
		}
		else {
			j--;
		}
	}

	cout << s << endl;
}

int main() {
	cin >> a >> b;
	LCS();
	getLCS();
	return 0;
}

相關文章