版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。

上一节我们讲了动态规划,我们也知道,动态规划对于子问题重叠的情况特别有效,因为它将子问题的解保存在存储空间中,当需要某个子问题的解时,直接取值即可,从而避免重复计算!

这一节我们来解决一个问题,就是最长公共子序列。

一、啥叫最长公共子序列?

【百度百科】LCS是Longest Common Subsequence的缩写,即最长公共子序列。一个序列,如果是两个或多个已知序列的子序列,且是所有子序列中最长的,则为最长公共子序列

在两个字符串中,有些字符会一样,形成的子序列也有可能相等,因此,长度最长的相等子序列便是两者间的最长公共字序列,其长度可以使用动态规划来求。

比如,对于字符串str1:"aabcd";有顺序且相互相邻的aabc是其子序列,有顺序但是不相邻的abd也是其子序列。即,只要得出序列中各个元素属于所给出的数列,就是子序列。

再来一个字符串str2:"12abcabcd";对比可以得出str1和str2的最长公共子序列是abcd。

得出结论:

  1. 子序列不是子集,它和原始序列的元素顺序是相关的。
  2. 空序列是任何两个序列的公共子序列。
  3. 子序列、公共子序列以及最长公共子序列都不唯一。
  4. 对于一个长度为n的序列,它一共有2^n 个子序列,有(2^n – 1)个非空子序列。

二、P问题和NP问题

P问题:一个问题可以在多项式(O(n^k))的时间复杂度内解决。
NP问题:一个问题的解可以在多项式的时间内被验证。

用人话来解释:

P问题:一个问题可以在多项式(O(n^k))的时间复杂度内解决。
NP问题:一个问题的解可以在多项式的时间内被验证。

三、最长公共子序列的解决办法

PS:可以使用递归去蛮力解决,需要遍历出所有的可能,时间复杂度是O(2^m*2^n),太慢了。

对于一般的LCS问题,都属于NP问题。当数列的量为一定的时,都可以采用动态规划去解决。时间复杂度时O(n * m),空间也是O(n * m)。

1.分析规律

对于可用动态规划求解的问题,一般有两个特征:①最优子结构;②重叠子问题

①最优子结构

设 X=(x1,x2,.....xn) 和 Y={y1,y2,.....ym} 是两个序列,将 X 和 Y 的最长公共子序列记为LCS(X,Y)

找出LCS(X,Y)就是一个最优化问题。因为,我们需要找到X 和 Y中最长的那个公共子序列。而要找X 和 Y的LCS,首先考虑X的最后一个元素和Y的最后一个元素。

1)如果 xn=ym,即X的最后一个元素与Y的最后一个元素相同,这说明该元素一定位于公共子序列中。因此,现在只需要找:LCS(Xn-1,Ym-1)

LCS(Xn-1,Ym-1)就是原问题的一个子问题。为什么叫子问题?因为它的规模比原问题小。(小一个元素也是小嘛....)

为什么是最优的子问题?因为我们要找的是Xn-1 和 Ym-1 的最长公共子序列啊。。。最长的!!!换句话说,就是最优的那个。(这里的最优就是最长的意思)

2)如果xn != ym,这下要麻烦一点,因为它产生了两个子问题:LCS(Xn-1,Ym) 和 LCS(Xn,Ym-1)

因为序列X 和 序列Y 的最后一个元素不相等嘛,那说明最后一个元素不可能是最长公共子序列中的元素嘛。(都不相等了,怎么公共嘛)。

LCS(Xn-1,Ym)表示:最长公共序列可以在(x1,x2,....x(n-1)) 和 (y1,y2,...yn)中找。

LCS(Xn,Ym-1)表示:最长公共序列可以在(x1,x2,....xn) 和 (y1,y2,...y(n-1))中找。

求解上面两个子问题,得到的公共子序列谁最长,那谁就是 LCS(X,Y)。用数学表示就是:

LCS=max{LCS(Xn-1,Ym),LCS(Xn,Ym-1)}

由于条件 1)  和  2)  考虑到了所有可能的情况。因此,我们成功地把原问题 转化 成了 三个规模更小的子问题。

②重叠子问题

重叠子问题是啥?就是说原问题 转化 成子问题后, 子问题中有相同的问题。

来看看,原问题是:LCS(X,Y)。子问题有 ❶LCS(Xn-1,Ym-1)    ❷LCS(Xn-1,Ym)    ❸LCS(Xn,Ym-1)

初一看,这三个子问题是不重叠的。可本质上它们是重叠的,因为它们只重叠了一大部分。举例:

第二个子问题:LCS(Xn-1,Ym) 就包含了:问题❶LCS(Xn-1,Ym-1),为什么?

因为,当Xn-1 和 Ym 的最后一个元素不相同时,我们又需要将LCS(Xn-1,Ym)进行分解:分解成:LCS(Xn-1,Ym-1) 和 LCS(Xn-2,Ym)

也就是说:在子问题的继续分解中,有些问题是重叠的。

2.做法

如果用一个二维数组c表示字符串X和Y中对应的前i,前j个字符的LCS的长度话,可以得到以下公式:

  1. 这个非常好理解,其中一个字符串为0的时候,那么肯定是0了。
  2. 当两个字符相等的时候,这个时候很好理解,举例来说:
  3. abcd 和 adcd,在遍历c的时候,发现前面只有a相等了,也就是1.
  4. 那么c相等,也就是abcadc在匹配的时候,一定比abad的长度大1,这个1就是c相等么。也就是相等的时候,是比c[i-1][j-1]1的。
  5. 下一个更好理解了,如果不相等,肯定就是找到上一个时刻对比最大的么。

因此,我们只需要从c[0][0]开始填表,填到c[m-1][n-1],所得到的c[m-1][n-1]就是LCS的长度。

但是,我们怎么得到LCS本身而非LCS的长度呢?也是用一个二维数组b来表示:

  • 在对应字符相等的时候,用↖标记
  • 在p1 >= p2的时候,用↑标记
  • 在p1 < p2的时候,用←标记

标记函数为:

比如说求ABCBDAB和BDCABA的LCS:

灰色且带↖箭头的部分即为所有的LCS的字符。就是一个填表过程。填好的表也就把子序列记录下来了,我们可以通过查表的方式得到你要的最长子序列。

这里可以看到,我们构造的一个i*j的矩阵,这个矩阵里的内容不但包括数值(当前结果的最优解),还包括一个方向箭头,这个代表了我们回溯的时候,需要行走的方向。

所以我们这里保存两个值,可以使用两个二维矩阵,也可以使用一个结构体矩阵。

四、演示下c数组的填表过程

以求ABCB和BDCA的LCS长度为例:

以此类推

最后填出的表为:

右下角的2即为LCS的长度。

五、实现代码

  1.  
  1. public class LongestCommonSubsequence {
  2. public static int [][]mem;
  3. public static int [][]s;
  4. public static int [] result; // 记录子串下标
  5. public static int LCS(char []X,char []Y,int n,int m){
  6. for (int i = 0; i <= n; i++) {
  7. mem[i][0] = 0;
  8. s[i][0] = 0;
  9. }
  10. for (int i = 0; i <= m; i++) {
  11. mem[0][i] = 0;
  12. s[0][i] = 0;
  13. }
  14. for (int i = 1; i <= n; i++) {
  15. for (int j = 1; j <= m ; j++) {
  16. if (X[i-1] == Y[j-1]){
  17. mem[i][j] = mem[i-1][j-1] + 1;
  18. s[i][j] = 1;
  19. }
  20. else {
  21. mem[i][j] = Math.max(mem[i][j-1],mem[i-1][j]);
  22. if (mem[i][j] == mem[i-1][j]){
  23. s[i][j] = 2;
  24. }
  25. else s[i][j] = 3;
  26. }
  27. }
  28. }
  29. return mem[n][m];
  30. }
  31. // 追踪解
  32. public static void trace_solution(int n,int m){
  33. int i = n;
  34. int j = m;
  35. int p = 0;
  36. while (true){
  37. if (i== 0 || j == 0) break;
  38. if (s[i][j] == 1 ){
  39. result[p] = i;
  40. p++;
  41. i--;j--;
  42. }
  43. else if (s[i][j] == 2){
  44. i--;
  45. }
  46. else { //s[i][j] == 3
  47. j--;
  48. }
  49. }
  50.  
  51. }
  52. public static void print(int [][]array,int n,int m){
  53. for (int i = 0; i < n + 1; i++) {
  54. for (int j = 0; j < m + 1; j++) {
  55. System.out.printf("%d ",array[i][j]);
  56. }
  57. System.out.println();
  58. }
  59. }
  60.  
  61. public static void main(String[] args) {
  62. char []X = {'A','B','C','B','D','A','B'};
  63. char []Y = {'B','D','C','A','B','A'};
  64. int n = X.length;
  65. int m = Y.length;
  66. // 这里重点理解,相当于多加了第一行 第一列。
  67. mem = new int[n+1][m+1];
  68. // 1 表示 左上箭头 2 表示 上 3 表示 左
  69. s = new int[n+1][m+1];
  70. result = new int[Math.min(n,m)];
  71. int longest = LCS(X,Y,n,m);
  72. System.out.println("备忘录表为:");
  73. print(mem,n,m);
  74. System.out.println("标记函数表为:");
  75. print(s,n,m);
  76. System.out.printf("longest : %d \n",longest);
  77.  
  78. trace_solution(n,m);
  79. // 输出注意 result 记录的是字符在序列中的下标
  80. for (int k = longest-1; k >=0 ; k--) {
  81. // 还需要再减一 才能跟 X Y序列对齐。
  82. int index = result[k]-1;
  83. System.out.printf("%c ",X[index]);
  84. }
  85.  
  86. }
  87. }
  1. 备忘录表为:
  2. 0 0 0 0 0 0 0
  3. 0 0 0 0 1 1 1
  4. 0 1 1 1 1 2 2
  5. 0 1 1 2 2 2 2
  6. 0 1 1 2 2 3 3
  7. 0 1 2 2 2 3 3
  8. 0 1 2 2 3 3 4
  9. 0 1 2 2 3 4 4
  10. 标记函数表为:
  11. 0 0 0 0 0 0 0
  12. 0 2 2 2 1 3 1
  13. 0 1 3 3 2 1 3
  14. 0 2 2 1 3 2 2
  15. 0 1 2 2 2 1 3
  16. 0 2 1 2 2 2 2
  17. 0 2 2 2 1 2 1
  18. 0 1 2 2 2 1 2
  19. longest : 4
  20. B C B A

六、总结

感觉没有讲到位,先挖坑在这里吧。

  1. 需要两个数组分别保存长度和具体的最长公共子序列的值
  2. 通过二维表的方式,把上一个结果存起来,后面只要查表就可以了
  3. git的diff算法是对最长公共子序列算法的延伸,性能更高

我的微信公众号:架构真经(id:gentoo666),分享Java干货,高并发编程,热门技术教程,微服务及分布式技术,架构设计,区块链技术,人工智能,大数据,Java面试题,以及前沿热门资讯等。每日更新哦!

参考资料:

  1. https://www.jianshu.com/p/cffe6217e13b
  2. https://blog.csdn.net/lz161530245/article/details/76943991
  3. https://www.cnblogs.com/xujian2014/p/4362012.html
  4. https://www.cnblogs.com/wkfvawl/p/9362287.html
  5. https://www.jianshu.com/p/b0172a3ac46c
  6. https://blog.csdn.net/weixin_40673608/article/details/84262695
  7. git diff比较
  8. https://blog.csdn.net/lxt_lucia/article/details/81209962
  9. https://blog.csdn.net/smilejiasmile/article/details/81503537

程序员的算法课(6)-最长公共子序列(LCS)的更多相关文章

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

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

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

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

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

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

  4. 1006 最长公共子序列Lcs

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

  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的最长公共子序列(子序列不要求是连续的). ...

随机推荐

  1. TensorFlow初学教程(完整版)

    1:你想要学习TensorFlow,首先你得安装Tensorflow,在你学习的时候你最好懂以下的知识:    a:怎么用python编程:     b:了解一些关于数组的知识:     c:最理想的 ...

  2. 初学android小笔记(一)

    一:应用外观基础设置 (1)去掉标题栏:打开Android Manifest文件,将theme如下设置 (2)改应用图标:将icon指定图片改为想要的app图标即可 (3)改应用名字: 在Manife ...

  3. Netty 入门,这一篇文章就够了

    Netty是Java领域有名的开源网络库,特点是高性能和高扩展性,因此很多流行的框架都是基于它来构建的,比如我们熟知的Dubbo.Rocketmq.Hadoop等,针对高性能RPC,一般都是基于Net ...

  4. NOIP201605玩具谜题-解题报告

    NOIP201605玩具谜题-解题报告                                                                         2019-11- ...

  5. javaScript中this到底指向谁

    1.前言 在JavaScript中,this的指向一直是大多数初学者的易错点,总是搞不清楚this到底指向谁,而在求职面试中,this的指向问题往往又是高频考点.本篇博文就来总结一下在JavaScri ...

  6. 基于 H5 Canvas 实现楼宇自控系统

    前言 楼宇自控是指楼宇中电力设备,如电梯.水泵.风机.空调等,其主要工作性质是强电驱动.通常这些设备是开放性的工作状态,也就是说没有形成一个闭环回路.只要接通电源,设备就在工作,至于工作状态.进程.能 ...

  7. SQL Server 中 `JSON_MODIFY` 的使用

    SQL Server 中 JSON_MODIFY 的使用 Intro SQL Server 从 2016 开始支持了一些 JSON操作,最近的项目里也是好多地方直接用字段直接存成了 JSON,需要了解 ...

  8. node.js评论列表和添加购物车数据库表创建

    2.1:评论列表--发表评论 用户点击新闻列表某一条新闻,看到新闻详细发表评论 -用户输入评论内容 -发表评论 [将用户评论内容保存数据库 xz_comment] 2.2:评论列表--发表评论-开发评 ...

  9. 架构设计:"4+1"视图

    概念 "4+1"视图,是指从5个不同视角来描述软件体系结构. "4+1"分别指: 逻辑视图 过程视图 物理视图 开发视图 场景/用例 视图 逻辑架构的描述可以围 ...

  10. [LC]206题 Reverse Linked List (反转链表)(链表)

    ①英文题目 Reverse a singly linked list. Example: Input: 1->2->3->4->5->NULL Output: 5-> ...