最长公共子序列LCS

问题定义

  • 子序列

    \[对于给定序列X=<x_1,x_2,...,x_m>,Z=<z_1,z_2,...z_k>\\ 存在一个严格递增的X的下标序列<i_1,i_2,...,i_k>,对所有j=1,2,...,k\\ 满足x_{i_j}=z_j \]

    则称Z为X的子序列

  • 公共子序列

    Z既是X的子序列,也是Y的子序列

  • 问题描述

    给定两个序列

    \[X=<x_1,x_2,...,x_m>,Y=<y_1,y_2,...,y_n> \]

    求X和Y的长度最长的公共子序列

应用动态规划算法

1. 刻画最长公共子序列的特征

LCS最优子结构

令$$X=<x_1,x_2,...,x_m>,Y=<y_1,y_2,...,y_n>$$为两个序列,$$Z=<z_1,z_2,...,z_k>$$是X和Y的任意LCS

  • 如果\(x_m=y_n,则z_k=x_m=y_n且Z_{k-1}是X_{m-1}和Y_{n-1}的一个LCS\)
  • 如果\(x_m\ne y_n,那么z_k\ne x_m\implies Z是X_{m-1}和Y的一个LCS\)
  • 如果\(x_m\ne y_n,那么z_k\ne y_n\implies Z是X和Y_{n-1}的一个LCS\)

2. 一个递归解

\[c[i,j]=\begin{cases} 0\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\;\;若i=1或j=0\\ c[i-1,j-1]+1\quad\quad\quad\quad\;\;\,若i,j>0且x_i=y_i\\ max(c[i,j-1],c[i-1,j])\quad 若i,j>0且x_i\ne y_i \end{cases} \]

3. 计算LCS的长度

 LCS-LENGTH(X,Y)
 m <- X.length
 n <- Y.length
 
let c[0...m][0...n] be new table

for i<-1 to m do
	c[i,0] <- 0
for i<-1 to n do
	c[0,j] <- 0

for i<-1 to m do
	for j<-1 to n do
		if X[i]==Y[j] do
			c[i][j] <- c[i-1][j-1] + 1
		else 
			c[i][j] <- max{c[i-1][j], c[i][j-1]}
 return c

4. 构造LCS

这里没有使用多余的表来存储顺序,而是通过\(c[i-1][j],c[i][j-1],c[i-1][j-1]\)的大小关系来判断顺序

  • 如果\(c[i][j]==0\),则结束
  • 如果\(c[i-1][j]==c[i][j]\),则对\(i=i-1,j=j\)继续
  • 如果\(c[i][j-1]==c[i][j]\),则对\(i=1,j=j-1\)继续
  • 如果\(c[i-1][j-1]+1==c[i][j]\),则对\(i=i-1,j=j-1\)继续,并输出\(X[i]\)
PRINT-LCS(X, c, i, j)
if c[i][j] == 0 do
	return

/* 如果c[i-1][j]==c[i][j],则对i=i-1,j=j继续 */
if c[i-1][j] == c[i][j] do
	PRINT-LCS(X, c, i-1, j)
/* 如果c[i][j-1]==c[i][j],则对i=i,j=j-1继续 */
else if c[i][j-1] == c[i][j] do
	PRINT-LCS(X, c, i, j-1)
/* 如果c[i-1][j-1]+1==c[i][j],则对i=i-1,j=j-1继续 */
else
	PRINT-LCS(X, c, i-1, j-1)
	print(X[i])

代码实现

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

int my_max(int a, int b) {
    return (a >= b) ? a : b;
}

vector<vector<int>> lcs_length(string X, string Y)
{
    int m = X.size();
    int n = Y.size();
    vector<vector<int>>c(m+1);
    for (int i = 0; i <= m; i++) {
        c[i].resize(n+1);
    }
    for (int i = 1; i <= m; i++) {
        c[i][0] = 0;
    }
    for (int j = 1; j <= n; j++) {
        c[0][j] = 0;
    }
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            /* 注意对应X和Y的索引 */
            if (X[i-1] == Y[j-1]) {
                c[i][j] = c[i - 1][j - 1] + 1;
            }
            else {
                c[i][j] = my_max(c[i][j - 1], c[i - 1][j]);
            }
        }
    }

    return c;
}

void print_lcs(string X, vector<vector<int>> c, int i, int j)
{
    if (c[i][j] == 0) {
        return;
    }
    
    if (c[i-1][j] == c[i][j]) {
        print_lcs(X, c, i - 1, j);
    }
    else if (c[i][j - 1] == c[i][j]) {
        print_lcs(X, c, i, j - 1);
    }
    else {
        print_lcs(X, c, i - 1, j - 1);
        cout << X[i - 1];
    }
}

int main()
{
    string X;
    string Y;
    cin >> X;
    cin >> Y;
    int m = X.size();
    int n = Y.size();
    vector<vector<int>> c = lcs_length(X, Y);

    cout << c[m][n] << endl;
    print_lcs(X, c, m, n);
}

算法改进

二维数组

对于计算某个位置的\(c[i][j]\)只需要三个表项:\(c[i-1][j-1],c[i-1][j],c[i][j-1]\),所以其实只需要当前一行和上一行的表项数值,故利用利用一个二维数组记录当前行和上一行的数值即可

\(c[i-1][j-1]\) $c[i-1][j] $
\(c[i][j-1]\) $c[i][j] $

伪代码

/* 假设X长度m<=Y长度n */
LCS-LENGTH(X, Y)
m <- X.length
n <- Y.length
let c[2][m] be new table
for i<-1 to n do
	for j<-1 to m do
		if X[j] == Y[i] do
			c[(i+1)%2][j] <- c[i%2][j-1]+1
		else
			c[(i+1)%2][j] <- max{c[(i+1)%2][j-1], c[i%2][j]}

return c[n%2][m]

代码实现

int lcs_pro(string X, string Y)
{
    int m = X.size();
    int n = Y.size();
    vector<vector<int>> c(2);
    c[1].resize(m + 1);
    c[0].resize(m + 1);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (X[j - 1] == Y[i - 1]) {
                c[i%2][j] = c[(i-1)%2][j - 1] + 1;
            }
            else {
                c[i%2][j] = my_max(c[i%2][j - 1], c[(i-1)%2][j]);
            }
        }
    }

    return c[(n-1)%2][m];
}

一维数组

对于计算某个位置的\(c[i][j]\)需要三个表项:

\[c[i-1][j-1],c[i-1][j],c[i][j-1] \]

如果在一行中表示,对于某个待计算的\(c[j]\)其中:

\[c[i-1][j]=未覆盖前的c[j]\\c[i][j-1]=c[j]\\c[i-1][j-1]=覆盖之前的c[j-1] \]

所以在一行中\(c[i-1][j]和c[i]][j-1]\)已经存在,只需要额外一个空间保存被覆盖的\(c[i-1][j-1]\)即可

被覆盖\(c[i-1,j-1]=pre\)
\(c[i][j-1]=c[j-1]\) \(c[i-1][j]=c[j]\)
待写入\(c[i,j]\)

伪代码

/* 假设X长度m<=Y长度n */
LCS-LENGTH(X, Y)
m <- X.length
n <- Y.length
let c[m] be new table
/* pre记录被前一个覆盖的值 */
pre <- 0
for i<-1 to n do
	for j<-1 to m do
		/* tmp临时记录被覆盖的值 */
		tmp <- c[j]
		if X[j] == Y[i] do
			c[j] = pre+1
		else
			c[j] = max{c[j], c[j-1]}
		pre <- tmp

return c[m]

代码实现

int lcs_plus(string X, string Y)
{
    int m = X.size();
    int n = Y.size();
    int pre = 0;
    vector<int> c(m + 1);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            int tmp = c[j];
            if (X[j - 1] == Y[i - 1]) {
                c[j] = pre + 1;
            }
            else {
                c[j] = my_max(c[j - 1], c[j]);
            }
            pre = tmp;
        }
    }

    return c[m];
}
posted @ 2020-04-24 10:36  李义山  阅读(148)  评论(0)    收藏  举报