动态规划法(十)最长公共子序列(LCS)问题

问题介绍

  给定一个序列X=<x_1,x_2,....,x_m>,另一个序列Z=<z_1,z_2,....,z_k>满足如下条件时称为X的子序列:存在一个严格递增的X的下标序列<i_1,i_2,...,i_k>,对所有的j=1,2,...,k满足x_{i_j}=z_j.
  给定两个序列XY,如果Z同时是XY的子序列,则称ZXY公共子序列最长公共子序列(LCS)问题指的是:求解两个序列XY的长度最长的公共子序列。例如,序列X=<A,B,C,B,D,A,B>Y=<B,D,C,A,B,A>的最长公共子序列为<B,C,B,A>,长度为4。
  本文将具体阐释如何用动态规划法(Dynamic Programming)来求解最长公共子序列(LCS)问题。

算法分析

1. LCS的子结构

  给定一个序列X=<x_1,x_2,....,x_m>,对i=0,1,...,m,定义X的第i前缀为X_i=<x_1,x_2,....,x_i>,其中X_0为空序列。
  (LCS的子结构)令X=<x_1,x_2,....,x_m>Y=<y_1,y_2,....,y_n>为两个序列,Z=<z_1,z_2,....,z_k>XY的任意LCS,则:

  1. 如果x_m=y_n,z_k=x_m=y_nZ_{k-1}X_{m-1}Y_{n-1}的一个LCS。
  2. 如果x_m\neq y_n,z_k \neq x_m意味着Z_{k-1}X_{m-1}Y的一个LCS。
  3. 如果x_m\neq y_n,z_k\neq y_nZ_{k-1}XY_{n-1}的一个LCS。

2. 构造递归解

  在求X=<x_1,x_2,....,x_m>Y=<y_1,y_2,....,y_n>的一个LCS时,需要求解一个或两个子问题:如果x_m=y_n,应求解X_{m-1}Y_{n-1}的一个LCS,再将x_m=y_n追加到这个LCS的末尾,就得到XY的一个LCS;如果x_m\neq y_n,需求解X_{m-1}Y的一个LCS与XY_{n-1}的一个LCS,两个LCS较长者即为XY的一个LCS。当然,可以看出,LCS问题容易出现重叠子问题,这时候,就需要用动态规划法来解决。
  定义c[i,j]表示X_iY_j的LCS的长度。如果i=0j=0,则c[i,j]=0.利用LCS的子结构,可以得到如下公式:

c[i,j]=\left\{ \begin{array}{lr} 0,\qquad 若i=0或j=0\\ c[i-1, j-1]+1,\qquad 若i,j>0且x_i=y_j\\ \max(c[i, j-1], c[i-1, j]),\qquad 若i,j>0且x_i\neq y_j \end{array} \right.

3. 计算LCS的长度

  计算LCS长度的伪代码为LCS-LENGTH. 过程LCS-LENGTH接受两个子序列X=<x_1,x_2,....,x_m>Y=<y_1,y_2,....,y_n>为输入。它将c[i, j]的值保存在表c中,同时,维护一个表b,帮助构造最优解。过程LCS-LENGTH的伪代码如下:

LCS-LENGTH(X, Y):
m = X.length
n = Y.length
let b[1...m, 1...n] and c[0...m, 0...n] be new table

for i = 1 to m
    c[i, 0] = 0
for j = 1 to n
    c[0, j] = 0

for i = 1 to m
    for j = 1 to n
        if x[i] == y[j]
           c[i,j] = c[i-1, j-1]+1
           b[i,j] = 'diag'
           
        elseif c[i-1, j] >= c[i, j-1]
            c[i,j] = c[i-1, j]
            b[i,j] = 'up'
            
        else
            c[i,j] = c[i, j-1]
            b[i,j] = 'left'
            
return c and b

4. 寻找LCS

  为了寻找XY的一个LCS, 我们需要用到LCS-LENGTH过程中的表b,只需要简单地从b[m, n]开始,并按箭头方向追踪下去即可。当在表项b[i,j]中遇到一个'diag'时,意味着x_i=y_j是LCS的一个元素。按照这种方法,我们可以按逆序依次构造出LCS的所有元素。伪代码PRINT-LCS如下:

PRINT-LCS(b, X, i, j):
    if i == 0 or j == 0
        return
    if b[i,j] == 'diag'
        PRINT-LCS(b, X, i-1, j-1)
        print x[i]
    elseif b[i,j] == 'up':
        PRINT-LCS(b, X, i-1, j)
    else
        PRINT-LCS(b, X, i, j-1)

程序实现

  有了以上对LCS问题的算法分析,我们不难写出具体的程序来实现它。下面将会给出Python代码和Java代码,供读者参考。
  完整的Python代码如下:

import numpy as np

# using dynamic programming to solve LCS problem
# parameters: X,Y -> list
def LCS_LENGTH(X, Y):
    m = len(X) # length of X
    n = len(Y) # length of Y

    # create two tables, b for directions, c for solution of sub-problem
    b = np.array([[None]*(n+1)]*(m+1))
    c = np.array([[0]*(n+1)]*(m+1))

    # use DP to sole LCS problem
    for i in range(1, m+1):
        for j in range(1, n+1):
            if X[i-1] == Y[j-1]:
                c[i,j] = c[i-1,j-1]+1
                b[i,j] = 'diag'
            elif c[i-1,j] >= c[i, j-1]:
                c[i,j] = c[i-1,j]
                b[i,j] = 'up'
            else:
                c[i,j] = c[i,j-1]
                b[i,j] = 'left'
    #print(b)
    #print(c)
    return b,c

# print longest common subsequence of X and Y
def print_LCS(b, X, i, j):

    if i == 0 or j == 0:
        return None
    if b[i,j] == 'diag':
        print_LCS(b, X, i-1, j-1)
        print(X[i-1], end=' ')
    elif b[i,j] == 'up':
        print_LCS(b, X, i-1, j)
    else:
        print_LCS(b, X, i, j-1)

X = 'conservatives'
Y = 'breather'

b,c = LCS_LENGTH(X,Y)
print_LCS(b, X, len(X), len(Y))

输出结果如下:

e a t e 

  完整的Java代码如下:

package DP_example;

import java.util.Arrays;
import java.util.List;

public class LCS {
    // 主函数
    public static void main(String[] args) {
        // 两个序列X和Y
        List<String> X = Arrays.asList("A","B","C","B","D","A","B");
        List<String> Y = Arrays.asList("B","D","C","A","B","A");

        int m = X.size(); //X的长度
        int n = Y.size(); // Y的长度
        String[][] b = LCS_length(X, Y); //获取维护表b的值

        print_LCS(b, X, m, n); // 输出LCS
    }

    /*
    函数LCS_length:获取维护表b的值
    传入参数: 两个序列X和Y
    返回值: 维护表b
     */
    public static String[][] LCS_length(List X, List Y){
        int m = X.size(); //X的长度
        int n = Y.size(); // Y的长度
        int[][] c = new int[m+1][n+1];
        String[][] b = new String[m+1][n+1];

        // 对表b和表c进行初始化
        for(int i=1; i<m+1; i++){
            for(int j=1; j<n+1; j++){
                c[i][j] = 0;
                b[i][j] = "";
            }
        }
        
        // 利用自底向上的动态规划法获取b和c的值
        for(int i=1; i<m+1; i++){
            for(int j=1; j<n+1; j++){
                if(X.get(i-1) == Y.get(j-1)){
                    c[i][j] = c[i-1][j-1]+1;
                    b[i][j] = "diag";
                }
                else if(c[i-1][j] >= c[i][j-1]){
                    c[i][j] = c[i-1][j];
                    b[i][j] = "up";
                }
                else{
                    c[i][j] = c[i][j-1];
                    b[i][j] = "left";
                }
            }
        }

        return b;
    }

    // 输出最长公共子序列
    public static int print_LCS(String[][] b, List X, int i, int j){

        if(i == 0 || j == 0)
            return 0;

        if(b[i][j].equals("diag")){
            print_LCS(b, X, i-1, j-1);
            System.out.print(X.get(i-1)+" ");
        }
        else if(b[i][j].equals("up"))
            print_LCS(b, X, i-1, j);
        else
            print_LCS(b, X, i, j-1);

        return 1;
    }
}

输出结果如下:

B C B A 

参考文献

  1. 算法导论(第三版) 机械工业出版社
  2. https://www.geeksforgeeks.org/longest-common-subsequence/

注意:本人现已开通两个微信公众号: 因为Python(微信号为:python_math)以及轻松学会Python爬虫(微信号为:easy_web_scrape), 欢迎大家关注哦~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容