数据结构【动态规划-二维数组】| leetcode 1143. 最长公共子序列(中等)

参考链接:

LCS:long common substring

本题目可以应用于DNA领域。

最长公共子序列问题是典型的二维动态规划问题。

假设字符串 t e x t 1 \rm text_{1} text1 t e x t 2 \rm text_{2} text2 的长度分别为 m m m n n n,创建 m + 1 m+1 m+1 n + 1 n+1 n+1 列的二维数组 d p dp dp ,其中 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示 text 1 [ 0 : i ] \text {text}_{1}[0: i] text1[0:i] text 2 [ 0 : j ] \text {text}_{2}[0: j] text2[0:j] 的最长公共子序列的长度。

上述表示中, text 1 [ 0 : i ] \text {text}_{1}[0: i] text1[0:i]表示 t e x t 1 \rm text_{1} text1 的长度为 i i i 的前缀, text 2 [ 0 : j ] \text {text}_{2}[0: j] text2[0:j] 表示 t e x t 1 \rm text_{1} text1 的长度为 j j j 的前缀。

(注: text 1 [ 0 : i − 1 ] \text {text}_1[0:i-1] text1[0:i1] 表示的是 text 1 \text {text}_1 text1 的 第 0 0 0 个元素到第 i − 1 i - 1 i1 个元素,两端都包含)

之所以 d p [ i ] [ j ] dp[i][j] dp[i][j] 的定义不是 text 1 [ 0 : i ] \text {text}_1[0:i] text1[0:i] text 2 [ 0 : j ] \text {text}_2[0:j] text2[0:j],是为了方便当 i = 0 i = 0 i=0 或者 j = 0 j = 0 j=0 的时候, d p [ i ] [ j ] dp[i][j] dp[i][j]表示的为空字符串和另外一个字符串的匹配,这样 d p [ i ] [ j ] dp[i][j] dp[i][j]可以初始化为 0 0 0

考虑动态规划的边界情况:

  • i = 0 i=0 i=0 时, text 1 [ 0 : i ] \text {text}_{1}[0: i] text1[0:i] 为空,空字符串和任何字符串的最长公共子序列的长度都是 0 ,因此对任意 0 ≤ j ≤ n 0 \leq j \leq n 0jn ,有 d p [ 0 ] [ j ] = 0 dp[0][j]=0 dp[0][j]=0

  • j = 0 j=0 j=0 时, text 2 [ 0 : j ] \text {text}_{2}[0: j] text2[0:j] 为空,同理可得,对任意 0 ≤ i ≤ m 0 \leq i \leq m 0im ,有 d p [ i ] [ 0 ] = 0 d p[i][0]=0 dp[i][0]=0

因此动态规划的边界情况是:当 i = 0 i=0 i=0 j = 0 j=0 j=0 时, d p [ i ] [ j ] = 0 dp[i][j]=0 dp[i][j]=0

i > 0 i>0 i>0 j > 0 j>0 j>0 时,考虑 d p [ i ] [ j ] dp[i][j] dp[i][j] 的计算:

  • text 1 [ i − 1 ] = text 2 [ j − 1 ] \text {text}_{1}[i-1]=\text {text}_{2}[j-1] text1[i1]=text2[j1] 时,将这两个相同的字符称为公共字符,考虑 text 1 [ 0 : i − 1 ] \text {text}_{1}[0: i-1] text1[0:i1] text [ 0 : j − 1 ] \text {text} [0: j-1] text[0:j1] 的最长公共子序列,再增加一个字符(即公共字符)即可得到 text 1 [ 0 : i ] \text {text}_{1}[0: i] text1[0:i] text [ 0 : j ] \text {text}[0: j] text[0:j] 的最长公共子序列,因此 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 d p[i][j]=d p[i-1][j-1]+1 dp[i][j]=dp[i1][j1]+1

  • text 1 [ i − 1 ] ≠ t e x t 2 [ j − 1 ] \text {text}_{1}[i-1] \neq t e x t_{2}[j-1] text1[i1]=text2[j1] 时,考虑以下两项:

    • text 1 [ 0 : i − 1 ] \text {text}_1[0: i-1] text1[0:i1] text 2 [ 0 : j ] \text {text}_{2}[0: j] text2[0:j] 的最长公共子序列;
    • text 1 [ 0 : i ] \text {text}_1[0: i] text1[0:i] text 2 [ 0 : j − 1 ] \text {text}_{2}[0: j-1] text2[0:j1] 的最长公共子序列。

要得到 text 1 [ 0 : i ] \text {text}_{1}[0: i] text1[0:i] text 2 [ 0 : j ] \text {text}_{2}[0: j] text2[0:j] 的最长公共子序列,应取两项中的长度较大的一项,因此 d p [ i ] [ j ] = max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) dp[i][j]=\max (dp[i-1][j], dp[i][j-1]) dp[i][j]=max(dp[i1][j],dp[i][j1])

由此可以得到如下状态转移方程:

d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] + 1 ,  text  1 [ i − 1 ] = text 2 [ j − 1 ] max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) ,  text  1 [ i − 1 ] ≠ text 2 [ j − 1 ] dp[i][j]= \begin{cases} dp[i-1][j-1]+1, & \text { text }_{1}[i-1]=\text {text}_{2}[j-1] \\ \max (d p[i-1][j], d p[i][j-1]), & \text { text }_{1}[i-1] \neq \text {text}_{2}[j-1] \end{cases} dp[i][j]={dp[i1][j1]+1,max(dp[i1][j],dp[i][j1]), text 1[i1]=text2[j1] text 1[i1]=text2[j1]

最终计算得到 d p [ m ] [ n ] dp[m][n] dp[m][n] 即为 t e x t 1 \rm text_1 text1 t e x t 2 \rm text_2 text2 的最长公共子序列的长度。
在这里插入图片描述

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        dp = [[0] * (len(text2)+1) for _ in range(len(text1)+1)]

        for i in range(1, len(text1)+1):
            for j in range(1, len(text2)+1):
                if text1[i-1] == text2[j-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])

        return dp[len(text1)][len(text2)]

方法二:贪心 + 二分查找

考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。

基于上面的贪心思路,我们维护一个数组 d [ i ] d[i] d[i],表示长度为 i i i 的最长上升子序列的末尾元素的最小值,用 l e n len len 记录目前最长上升子序列的长度,起始时 l e n len len 为 1, d [ 1 ] = nums [ 0 ] d[1] = \text{nums}[0] d[1]=nums[0]

同时我们可以注意到 d [ i ] d[i] d[i] 是关于 i i i 单调递增的。因为如果 d [ j ] ≥ d [ i ] d[j] \geq d[i] d[j]d[i] j < i j < i j<i,我们考虑从长度为 i i i 的最长上升子序列的末尾删除 i − j i-j ij 个元素,那么这个序列长度变为 j j j,且第 j j j 个元素 x x x(末尾元素)必然小于 d [ i ] d[i] d[i],也就小于 d [ j ] d[j] d[j]。那么我们就找到了一个长度为 j j j 的最长上升子序列,并且末尾元素比 d [ j ] d[j] d[j] 小,从而产生了矛盾。因此数组 d d d 的单调性得证。

我们依次遍历数组 nums \text{nums} nums 中的每个元素,并更新数组 d d d l e n len len 的值。如果 nums [ i ] > d [ len ] \text{nums}[i] > d[\text{len}] nums[i]>d[len] 则更新 l e n = l e n + 1 len = len + 1 len=len+1,否则在 d [ 1 … l e n ] d[1 \ldots len] d[1len] 中找满足 d [ i − 1 ] < nums [ j ] < d [ i ] d[i - 1] < \text{nums}[j] < d[i] d[i1]<nums[j]<d[i] 的下标 i i i,并更新 d [ i ] = nums [ j ] d[i] = \text{nums}[j] d[i]=nums[j]

根据 d d d 数组的单调性,我们可以使用二分查找寻找下标 i i i,优化时间复杂度。

最后整个算法流程为:

设当前已求出的最长上升子序列的长度为 len \text{len} len(初始时为 1),从前往后遍历数组 nums \text{nums} nums,在遍历到 nums [ i ] \text{nums}[i] nums[i] 时:

如果 nums [ i ] > d [ len ] \text{nums}[i] > d[\text{len}] nums[i]>d[len],则直接加入到 d d d 数组末尾,并更新 len = len + 1 \text{len} = \text{len} + 1 len=len+1

否则,在 d d d 数组中二分查找,找到第一个比 nums [ i ] \text{nums}[i] nums[i] 小的数 d [ k ] d[k] d[k],并更新 d [ k + 1 ] = nums [ i ] d[k + 1] = \text{nums}[i] d[k+1]=nums[i]

以输入序列 [0, 8, 4, 12, 2] 为例:

第一步插入 0,d = [0];

第二步插入 8,d = [0, 8];

第三步插入 4,d = [0, 4];

第四步插入 12,d = [0, 4, 12];

第五步插入 2,d = [0, 2, 12]。

最终得到最大递增子序列长度为 3。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值