问题介绍

  给定一个序列\(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.\)

  给定两个序列\(X\)和\(Y\),如果\(Z\)同时是\(X\)和\(Y\)的子序列,则称\(Z\)是\(X\)和\(Y\)的公共子序列最长公共子序列(LCS)问题指的是:求解两个序列\(X\)和\(Y\)的长度最长的公共子序列。例如,序列\(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>\)为\(X\)和\(Y\)的任意LCS,则:

  1. 如果\(x_m=y_n,\)则\(z_k=x_m=y_n\)且\(Z_{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_n\)且\(Z_{k-1}\)是\(X\)和\(Y_{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的末尾,就得到\(X\)和\(Y\)的一个LCS;如果\(x_m\neq y_n\),需求解\(X_{m-1}\)和\(Y\)的一个LCS与\(X\)和\(Y_{n-1}\)的一个LCS,两个LCS较长者即为\(X\)和\(Y\)的一个LCS。当然,可以看出,LCS问题容易出现重叠子问题,这时候,就需要用动态规划法来解决。

  定义\(c[i,j]\)表示\(X_i\)和\(Y_j\)的LCS的长度。如果\(i=0\)或\(j=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

  为了寻找\(X\)和\(Y\)的一个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), 欢迎大家关注哦~~

动态规划法(十)最长公共子序列(LCS)问题的更多相关文章

  1. C++版 - Lintcode 77-Longest Common Subsequence最长公共子序列(LCS) - 题解

    版权声明:本文为博主Bravo Yeung(知乎UserName同名)的原创文章,欲转载请先私信获博主允许,转载时请附上网址 http://blog.csdn.net/lzuacm. C++版 - L ...

  2. 1006 最长公共子序列Lcs

    1006 最长公共子序列Lcs 基准时间限制:1 秒 空间限制:131072 KB 给出两个字符串A B,求A与B的最长公共子序列(子序列不要求是连续的). 比如两个串为: abcicba abdks ...

  3. 动态规划之最长公共子序列LCS(Longest Common Subsequence)

    一.问题描述 由于最长公共子序列LCS是一个比较经典的问题,主要是采用动态规划(DP)算法去实现,理论方面的讲述也非常详尽,本文重点是程序的实现部分,所以理论方面的解释主要看这篇博客:http://b ...

  4. 编程算法 - 最长公共子序列(LCS) 代码(C)

    最长公共子序列(LCS) 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 题目: 给定两个字符串s,t, 求出这两个字符串最长的公共子序列的长度. 字符 ...

  5. POJ 1458 Common Subsequence(最长公共子序列LCS)

    POJ1458 Common Subsequence(最长公共子序列LCS) http://poj.org/problem?id=1458 题意: 给你两个字符串, 要你求出两个字符串的最长公共子序列 ...

  6. 51Nod 1006:最长公共子序列Lcs(打印LCS)

    1006 最长公共子序列Lcs  基准时间限制:1 秒 空间限制:131072 KB 分值: 0 难度:基础题  收藏  关注 给出两个字符串A B,求A与B的最长公共子序列(子序列不要求是连续的). ...

  7. 51nod 1006 最长公共子序列Lcs 【LCS/打印path】

    1006 最长公共子序列Lcs  基准时间限制:1 秒 空间限制:131072 KB 分值: 0 难度:基础题  收藏  关注 给出两个字符串A B,求A与B的最长公共子序列(子序列不要求是连续的). ...

  8. 每日一题-——最长公共子序列(LCS)与最长公共子串

    最长公共子序列(LCS) 思路: 代码: def LCS(string1,string2): len1 = len(string1) len2 = len(string2) res = [[0 for ...

  9. 51nod 1006:最长公共子序列Lcs

    1006 最长公共子序列Lcs 基准时间限制:1 秒 空间限制:131072 KB 分值: 0 难度:基础题  收藏  关注 给出两个字符串A B,求A与B的最长公共子序列(子序列不要求是连续的). ...

  10. 51nod 1006 最长公共子序列Lcs(经典动态规划)

    传送门 Description 给出两个字符串A B,求A与B的最长公共子序列(子序列不要求是连续的).   比如两个串为:   abcicba abdkscab   ab是两个串的子序列,abc也是 ...

随机推荐

  1. php中出现乱码

    对于初学着来说,编辑中文php时,会出现乱码 在php代码中加入 随后在浏览器中,就会看到如下页面 这样就解决了php 中文乱码的问题.

  2. UniGui中使用Grid++Report报表控件子报表获取数据的方法

    Grid++Report是为优秀的报表控件,子报表是其重要功能之一,但Grid++Report提供的网页报表示范主要是以页面为主的,UniGui在Delphi中以快速编写web管理软件著称,但由于资料 ...

  3. 解决weblogic 部署环境后出现的乱码问题

    1.在startWebloci.cmd 中 set CLASSPATH=%SAVE_CLASSPATH% 下增加一行 JAVA_OPTIONS="${JAVA_OPTIONS} -Dfile ...

  4. Oneinstack 环境安装 Flarum 轻量级论坛程序

    之前做了个论坛,使用的是discuz框架,虽然这个框架已经很成熟,功能也相对较多,但毕竟老框架了,今天尝试用Flarum来搭建一个论坛,Flarum相对来说美观而且速度快 系统环境使用oneinsta ...

  5. silverlight属性改变事件通知

    工作中遇到silverlight本身没有提供的某些属性改变事件,但又需要在属性改变时得到通知,Google搬运stack overflow,原地址 /// Listen for change of t ...

  6. Redis学习笔记之延时队列

    目录 一.业务场景 二.Redis延时队列 一.业务场景 所谓延时队列就是延时的消息队列,下面说一下一些业务场景比较好理解 1.1 实践场景 订单支付失败,每隔一段时间提醒用户 用户并发量的情况,可以 ...

  7. Oracle创建表空间、用户管理、角色管理

    内容:Oracle创建表空间.用户管理.角色管理 1.用系统用户登录Oracle 默认的系统用户: sys/system.sysman.scott sys:权限最大,超级用户,可以完成所有任务, 默认 ...

  8. 一文了解Python中的循环(for while break continue 嵌套循环...)

    循环 目标 程序的三大流程 while 循环基本使用 break 和 continue while 循环嵌套 01. 程序的三大流程 在程序开发中,一共有三种流程方式: 顺序 —— 从上向下,顺序执行 ...

  9. [原创] 详解云计算网络底层技术——虚拟网络设备 tap/tun 原理解析

    本文首发于我的公众号 Linux云计算网络(id: cloud_dev),专注于干货分享,号内有 10T 书籍和视频资源,后台回复「1024」即可领取,欢迎大家关注,二维码文末可以扫. 在云计算时代, ...

  10. 两步验证杀手锏:Java 接入 Google 身份验证器实战

    两步验证 大家应该对两步验证都熟悉吧?如苹果有自带的两步验证策略,防止用户账号密码被盗而锁定手机进行敲诈,这种例子屡见不鲜,所以苹果都建议大家开启两步验证的. Google 的身份验证器一般也是用于登 ...