目录
基本概念
问题分析
程序实现
打印一个LCS
打印全部LCS
空间优化
危险的递归解法
首页 web前端 js教程 详谈javascript最长公共子序列

详谈javascript最长公共子序列

Jan 23, 2018 am 10:44 AM
javascript js 序列

最长公共子序是从给定的两个序列X和Y中取出尽可能多的一部分字符,按照它们在原序列排列的先后次序排列得到。LCS问题的算法用途广泛,如在软件不同版本的管理中,用LCS算法找到新旧版本的异同处;在软件测试中,用LCS算法对录制和回放的序列进行比较,在基因工程领域,用LCS算法检查患者DNA连与键康DNA链的异同;在防抄袭系统中,用LCS算法检查论文的抄袭率。LCS算法也可以用于程序代码相似度度量,人体运行的序列检索,视频段匹配等方面,所以对LCS算法进行研究具有很高的应用价值。

基本概念

  1. 子序列(subsequence): 一个特定序列的子序列就是将给定序列中零个或多个元素去掉后得到的结果(不改变元素间相对次序)。例如序列的子序列有:等。

  2. 公共子序列(common subsequence): 给定序列X和Y,序列Z是X的子序列,也是Y的子序列,则Z是X和Y的公共子序列。例如X=[A,B,C,B,D,A,B],Y=[B,D,C,A,B,A[,那么序列Z=[B,C,A]为X和Y的公共子序列,其长度为3。但Z不是X和Y的最长公共子序列,而序列[B,C,B,A]和[B,D,A,B]也均为X和Y的最长公共子序列,长度为4,而X和Y不存在长度大于等于5的公共子序列。对于序列[A,B,C]和序列[E,F,G]的公共子序列只有空序列[]。

  3. 最长公共子序列:给定序列X和Y,从它们的所有公共子序列中选出长度最长的那一个或几个。

  4. 子串: 将一个序列从最前或最后或同时删掉零个或几个字符构成的新系列。区别与子序列,子序列是可以从中间抠掉字符的。cnblogs这个字符串中子序列有多少个呢?很显然有27个,比如其中的cb,cgs等等都是其子序列

给一个图再解释一下:

详谈javascript最长公共子序列

我们可以看出子序列不见得一定是连续的,连续的是子串。

问题分析

我们还是从一个矩阵开始分析,自己推导出状态迁移方程。

首先,我们把问题转换成前端够为熟悉的概念,不要序列序列地叫了,可以认为是数组或字符串。一切从简,我们就估且认定是两个字符串做比较吧。

我们重点留意”子序列“的概念,它可以删掉多个或零个,也可以全部干掉。这时我们的第一个子序列为 空字符串 (如果我们的序列不是字符串,我们还可以 )!这个真是千万要注意到!许多人就是看不懂《算法导论》的那个图表,还有许多博客的作者不懂装懂。我们总是从左到右比较,当然了第一个字符串,由于作为矩阵的高,就垂直放置了。

x "" B D C A B A





""


A
B
C
D
A
B

假令X = "ABCDAB", Y="BDCABA",各自取出最短的序列,也就是空字符串与空字符串比较。LCS的方程解为一个数字,那么这个表格也只能填数字。两个空字符串的公同区域的长度为0.

x "" B D C A B A





"" 0


A
B
C
D
A
B

然后我们X不动,继续让空字符串出阵,Y让“B”出阵,很显然,它们的公共区域的长度为0. Y换成其他字符, D啊,C啊, 或者, 它们的连续组合DC、 DDC, 情况没有变, 依然为0. 因此第一行都为0. 然后我们Y不动,Y只出空字任串,那么与上面的分析一样,都为0,第一列都是0.

x "" B D C A B A





"" 0 0 0 0 0 0 0





A 0
B 0
C 0
D 0
A 0
B 0

LCS问题与背包问题有点不一样,背包问题还可以设置-1行,而最长公共子序列因为有空子序列的出现,一开始就把左边与上边固定死了。

然后我们再将问题放大些,这次双方都出一个字符,显然只有两都相同时,才有存在不为空字符串的公共子序列,长度也理解数然为1。

A为"X", Y为"BDCA"的子序列的任意一个

x "" B D C A B A





"" 0 0 0 0 0 0 0





A 0 0 0 0 1



B 0
C 0
D 0
A 0
B 0

继续往右填空,该怎么填?显然,LCS不能大于X的长度,Y的从A字符串开始的子序列与B的A序列相比,怎么也能等于1。

x "" B D C A B A





"" 0 0 0 0 0 0 0





A 0 0 0 0 1 1 1





B 0
C 0
D 0
A 0
B 0

如果X只从派出前面个字符A,B吧,亦即是“”,“A”, "B", "AB"这四种组合,前两个已经解说过了。那我们先看B,${X_1} == ${Y_0}, 我们得到一个新的公共子串了,应该加1。为什么呢?因为我们这个矩阵是一个状态表,从左到右,从上到下描述状态的迁移过程,并且这些状态是基于已有状态 累加 出来的。 现在我们需要确认的是,现在我们要填的这个格子的值与它周围已经填好的格子的值是存在何种关系 。目前,信息太少,就是一个孤点,直接填1。

x "" B D C A B A





"" 0 0 0 0 0 0 0





A 0 0 0 0 1 1 1





B 0 1
C 0
D 0
A 0
B 0

然后我们让Y多出一个D做帮手,{"",A,B,AB} vs {"",B,D,BD},显然,继续填1. 一直填到Y的第二个B之前,都是1。 因为到BDCAB时,它们有另一个公共子序列,AB。

x "" B D C A B A





"" 0 0 0 0 0 0 0





A 0 0 0 0 1 1 1





B 0 1 1 1 1 2



C 0
D 0
A 0
B 0

到这一步,我们可以总结一些规则了,之后就是通过计算验证我们的想法,加入新的规则或限定条件来完善。

详谈javascript最长公共子序列

Y将所有字符派上去,X依然是2个字符,经仔细观察,还是填2.

看五行,X再多派一个C,ABC的子序列集合比AB的子序列集合大一些,那么它与Y的B子序列集合大一些,就算不大,就不能比原来的小。显然新增的C不能成为战力,不是两者的公共字符,因此值应该等于AB的子序列集合。

× "" B D C A B A





"" 0 0 0 0 0 0 0





A 0 0 0 0 1 1 1





B 0 1 1 1 1 2 2





C 0 1
D 0
A 0
B 0

详谈javascript最长公共子序列

并且我们可以确定,如果两个字符串要比较的字符不一样,那么要填的格子是与其左边或上边有关,那边大就取那个。

如果比较的字符一样呢,稍安毋躁,刚好X的C要与Y的C进行比较,即ABC的子序列集合{"",A,B,C,AB,BC,ABC}与BDC的子序列集合{"",B,D,C,BD,DC,BDC}比较,得到公共子串有“”,B,D 。这时还是与之前的结论一样,当字符相等时,它对应的格子值等于左边与右边与左上角的值,并且左边,上边,左上边总是相等的。这些奥秘需要更严格的数学知识来论证。

假设有两个数组,A和B。A[i]为A的第i个元素,A(i)为由A的第一个元素到第i个元素所组成的前缀。m(i, j)为A(i)和B(j)的最长公共子序列长度。

由于算法本身的递推性质,其实只要证明,对于某个i和j:

  m(i, j) = m(i-1, j-1) + 1 (当A[i] = B[j]时)

  m(i, j) = max( m(i-1, j), m(i, j-1) ) (当A[i] != B[j]时)

第一个式子很好证明,即当A[i] = B[j]时。可以用反证,假设m(i, j) > m(i-1, j-1) + 1 (m(i, j)不可能小于m(i-1, j-1) + 1,原因很明显),那么可以推出m(i-1, j-1)不是最长的这一矛盾结果。

第二个有些trick。当A[i] != B[j]时,还是反证,假设m(i, j) > max( m(i-1, j), m(i, j-1) )。

由反证假设,可得m(i, j) > m(i-1, j)。这个可以推出A[i]一定在m(i, j)对应的LCS序列中(反证可得)。而由于A[i] != B[j],故B[j]一定不在m(i, j)对应的LCS序列中。所以可推出m(i, j) = m(i, j-1)。这就推出了与反正假设矛盾的结果。

得证。
登录后复制

 

我们现在用下面的方程来继续填表了。

详谈javascript最长公共子序列

程序实现

//by 司徒正美
function LCS(str1, str2){
        var rows =  str1.split("")
        rows.unshift("")
        var cols =  str2.split("")
        cols.unshift("")
        var m = rows.length 
        var n = cols.length 
        var dp = []
        for(var i = 0; i < m; i++){ 
            dp[i] = []
            for(var j = 0; j < n; j++){ 
                if(i === 0 || j === 0){
                    dp[i][j] = 0
                    continue
                }
              
                if(rows[i] === cols[j]){ 
                    dp[i][j] = dp[i-1][j-1] + 1 //对角+1
                }else{
                    dp[i][j] = Math.max( dp[i-1][j], dp[i][j-1]) //对左边,上边取最大
                }
            }
            console.log(dp[i].join(""))//调试
        } 
        return dp[i-1][j-1]
    }
登录后复制

LCS可以进一步简化,只要通过挪位置,省去新数组的生成

//by司徒正美
function LCS(str1, str2){
    var m = str1.length 
    var n = str2.length
    var dp = [new Array(n+1).fill(0)] //第一行全是0
    for(var i = 1; i <= m; i++){ //一共有m+1行
        dp[i] = [0] //第一列全是0
        for(var j = 1; j <= n; j++){//一共有n+1列
            if(str1[i-1] === str2[j-1]){ 
                //注意这里,str1的第一个字符是在第二列中,因此要减1,str2同理
                dp[i][j] = dp[i-1][j-1] + 1 //对角+1
            } else {
                 dp[i][j] = Math.max( dp[i-1][j], dp[i][j-1]) 
            }
        }
    } 
    return dp[m][n];
}
登录后复制

打印一个LCS

我们再给出打印函数,先看如何打印一个。我们从右下角开始寻找,一直找到最上一行终止。因此目标字符串的构建是倒序。为了避免使用stringBuffer这样麻烦的中间量,我们可以通过递归实现,每次执行程序时,只返回一个字符串,没有则返回一个空字符串, 以 printLCS(x,y,...) + str[i] 相加,就可以得到我们要求的字符串。

我们再写出一个方法,来验证我们得到的字符串是否真正的LCS字符串。作为一个已经工作的人,不能写的代码像在校生那样,不做单元测试就放到线上让别人踩坑。

//by 司徒正美,打印一个LCS
function printLCS(dp, str1, str2, i, j){
    if (i == 0 || j == 0){
        return "";
    }
    if( str1[i-1] == str2[j-1] ){
        return printLCS(dp, str1, str2, i-1, j-1) + str1[i-1];
    }else{
        if (dp[i][j-1] > dp[i-1][j]){
            return printLCS(dp, str1, str2, i, j-1);
        }else{
            return printLCS(dp, str1, str2, i-1, j);
        }
    }
}
//by司徒正美, 将目标字符串转换成正则,验证是否为之前两个字符串的LCS
function validateLCS(el, str1, str2){
   var re =  new RegExp( el.split("").join(".*") )
   console.log(el, re.test(str1),re.test(str2))
   return re.test(str1) && re.test(str2)
}
登录后复制

 

使用:

function LCS(str1, str2){
    var m = str1.length 
    var n = str2.length
    //....略,自行补充
    var s = printLCS(dp, str1, str2, m, n)
    validateLCS(s, str1, str2)
    return dp[m][n]
}
var c1 = LCS( "ABCBDAB","BDCABA");
console.log(c1) //4 BCBA、BCAB、BDAB
var c2 = LCS("13456778" , "357486782" );
console.log(c2) //5 34678 
var c3 = LCS("ACCGGTCGAGTGCGCGGAAGCCGGCCGAA" ,"GTCGTTCGGAATGCCGTTGCTCTGTAAA" );
console.log(c3) //20 GTCGTCGGAAGCCGGCCGAA
登录后复制

 

详谈javascript最长公共子序列

打印全部LCS

思路与上面差不多,我们注意一下,在LCS方法有一个Math.max取值,这其实是整合了三种情况,因此可以分叉出三个字符串。我们的方法将返回一个es6集合对象,方便自动去掉。然后每次都用新的集合合并旧的集合的字任串。

//by 司徒正美 打印所有LCS
function printAllLCS(dp, str1, str2, i, j){
    if (i == 0 || j == 0){
        return new Set([""])
    }else if(str1[i-1] == str2[j-1]){
        var newSet = new Set()
        printAllLCS(dp, str1, str2, i-1, j-1).forEach(function(el){
            newSet.add(el + str1[i-1])
        })
        return newSet
    }else{
        var set = new Set()
        if (dp[i][j-1] >= dp[i-1][j]){
            printAllLCS(dp, str1, str2, i, j-1).forEach(function(el){
              set.add(el)
            })
        }
        if (dp[i-1][j] >= dp[i][j-1]){//必须用>=,不能简单一个else搞定
            printAllLCS(dp, str1, str2, i-1, j).forEach(function(el){
              set.add(el)
            })
        }   
        return set
    } 
 }
登录后复制

 

使用:

function LCS(str1, str2){
    var m = str1.length 
    var n = str2.length
    //....略,自行补充
    var s =  printAllLCS(dp, str1, str2, m, n)
    console.log(s)
    s.forEach(function(el){
        validateLCS(el,str1, str2)
        console.log("输出LCS",el)
    })
    return dp[m][n]
}
var c1 = LCS( "ABCBDAB","BDCABA");
console.log(c1) //4 BCBA、BCAB、BDAB
var c2 = LCS("13456778" , "357486782" );
console.log(c2) //5 34678 
var c3 = LCS("ACCGGTCGAGTGCGCGGAAGCCGGCCGAA" ,"GTCGTTCGGAATGCCGTTGCTCTGTAAA" );
console.log(c3) //20 GTCGTCGGAAGCCGGCCGAA
登录后复制

 

详谈javascript最长公共子序列

空间优化

使用滚动数组:

function LCS(str1, str2){
    var m = str1.length 
    var n = str2.length
    var dp = [new Array(n+1).fill(0)],now = 1,row //第一行全是0
    for(var i = 1; i <= m; i++){ //一共有2行
        row = dp[now] = [0] //第一列全是0
        for(var j = 1; j <= n; j++){//一共有n+1列
            if(str1[i-1] === str2[j-1]){ 
                //注意这里,str1的第一个字符是在第二列中,因此要减1,str2同理
                dp[now][j] = dp[i-now][j-1] + 1 //对角+1
            } else {
                dp[now][j] = Math.max( dp[i-now][j], dp[now][j-1]) 
            }
        }
        now = 1- now; //1-1=>0;1-0=>1; 1-1=>0 ...
    } 
    return row ? row[n]: 0
}
登录后复制

 

危险的递归解法

str1的一个子序列相应于下标序列{1, 2, …, m}的一个子序列,因此,str1共有${2^m}$个不同子序列(str2亦如此,如为${2^n}$),因此复杂度达到惊人的指数时间(${2^m * 2^n}$)。

//警告,字符串的长度一大就会爆栈
function LCS(str1, str2, a, b) {
      if(a === void 0){
          a = str1.length - 1
      }
      if(b === void 0){
          b = str2.length - 1
      }
      if(a == -1 || b == -1){
          return 0
      } 
      if(str1[a] == str2[b]) {
         return LCS(str1, str2,  a-1, b-1)+1;
      }
      if(str1[a] != str2[b]) {
         var x =  LCS(str1, str2, a, b-1)
         var y =  LCS(str1, str2, a-1, b)
         return x >= y ? x : y
      }
  }
登录后复制

    相关推荐:

用Python语言描述最大连续子序列和

最大连续子序列和问题

PHP求最大子序列和的算法实现_PHP教程

以上是详谈javascript最长公共子序列的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1655
14
CakePHP 教程
1414
52
Laravel 教程
1307
25
PHP教程
1253
29
C# 教程
1228
24
如何使用WebSocket和JavaScript实现在线语音识别系统 如何使用WebSocket和JavaScript实现在线语音识别系统 Dec 17, 2023 pm 02:54 PM

如何使用WebSocket和JavaScript实现在线语音识别系统引言:随着科技的不断发展,语音识别技术已经成为了人工智能领域的重要组成部分。而基于WebSocket和JavaScript实现的在线语音识别系统,具备了低延迟、实时性和跨平台的特点,成为了一种被广泛应用的解决方案。本文将介绍如何使用WebSocket和JavaScript来实现在线语音识别系

推荐:优秀JS开源人脸检测识别项目 推荐:优秀JS开源人脸检测识别项目 Apr 03, 2024 am 11:55 AM

人脸检测识别技术已经是一个比较成熟且应用广泛的技术。而目前最为广泛的互联网应用语言非JS莫属,在Web前端实现人脸检测识别相比后端的人脸识别有优势也有弱势。优势包括减少网络交互、实时识别,大大缩短了用户等待时间,提高了用户体验;弱势是:受到模型大小限制,其中准确率也有限。如何在web端使用js实现人脸检测呢?为了实现Web端人脸识别,需要熟悉相关的编程语言和技术,如JavaScript、HTML、CSS、WebRTC等。同时还需要掌握相关的计算机视觉和人工智能技术。值得注意的是,由于Web端的计

WebSocket与JavaScript:实现实时监控系统的关键技术 WebSocket与JavaScript:实现实时监控系统的关键技术 Dec 17, 2023 pm 05:30 PM

WebSocket与JavaScript:实现实时监控系统的关键技术引言:随着互联网技术的快速发展,实时监控系统在各个领域中得到了广泛的应用。而实现实时监控的关键技术之一就是WebSocket与JavaScript的结合使用。本文将介绍WebSocket与JavaScript在实时监控系统中的应用,并给出代码示例,详细解释其实现原理。一、WebSocket技

股票分析必备工具:学习PHP和JS绘制蜡烛图的步骤 股票分析必备工具:学习PHP和JS绘制蜡烛图的步骤 Dec 17, 2023 pm 06:55 PM

股票分析必备工具:学习PHP和JS绘制蜡烛图的步骤,需要具体代码示例随着互联网和科技的快速发展,股票交易已经成为许多投资者的重要途径之一。而股票分析是投资者决策的重要一环,其中蜡烛图被广泛应用于技术分析中。学习如何使用PHP和JS绘制蜡烛图将为投资者提供更多直观的信息,帮助他们更好地做出决策。蜡烛图是一种以蜡烛形状来展示股票价格的技术图表。它展示了股票价格的

如何利用JavaScript和WebSocket实现实时在线点餐系统 如何利用JavaScript和WebSocket实现实时在线点餐系统 Dec 17, 2023 pm 12:09 PM

如何利用JavaScript和WebSocket实现实时在线点餐系统介绍:随着互联网的普及和技术的进步,越来越多的餐厅开始提供在线点餐服务。为了实现实时在线点餐系统,我们可以利用JavaScript和WebSocket技术。WebSocket是一种基于TCP协议的全双工通信协议,可以实现客户端与服务器的实时双向通信。在实时在线点餐系统中,当用户选择菜品并下单

如何使用WebSocket和JavaScript实现在线预约系统 如何使用WebSocket和JavaScript实现在线预约系统 Dec 17, 2023 am 09:39 AM

如何使用WebSocket和JavaScript实现在线预约系统在当今数字化的时代,越来越多的业务和服务都需要提供在线预约功能。而实现一个高效、实时的在线预约系统是至关重要的。本文将介绍如何使用WebSocket和JavaScript来实现一个在线预约系统,并提供具体的代码示例。一、什么是WebSocketWebSocket是一种在单个TCP连接上进行全双工

PHP与JS开发技巧:掌握绘制股票蜡烛图的方法 PHP与JS开发技巧:掌握绘制股票蜡烛图的方法 Dec 18, 2023 pm 03:39 PM

随着互联网金融的迅速发展,股票投资已经成为了越来越多人的选择。而在股票交易中,蜡烛图是一种常用的技术分析方法,它能够显示股票价格的变化趋势,帮助投资者做出更加精准的决策。本文将通过介绍PHP和JS的开发技巧,带领读者了解如何绘制股票蜡烛图,并提供具体的代码示例。一、了解股票蜡烛图在介绍如何绘制股票蜡烛图之前,我们首先需要了解一下什么是蜡烛图。蜡烛图是由日本人

JavaScript和WebSocket:打造高效的实时天气预报系统 JavaScript和WebSocket:打造高效的实时天气预报系统 Dec 17, 2023 pm 05:13 PM

JavaScript和WebSocket:打造高效的实时天气预报系统引言:如今,天气预报的准确性对于日常生活以及决策制定具有重要意义。随着技术的发展,我们可以通过实时获取天气数据来提供更准确可靠的天气预报。在本文中,我们将学习如何使用JavaScript和WebSocket技术,来构建一个高效的实时天气预报系统。本文将通过具体的代码示例来展示实现的过程。We

See all articles