动态规划-公共子序列问题

简介: 前言前面我们学习了动态规划的背包类型问题,其中涉及01背包,完全背包,多重背包。现在我们来继续学习动态规划的公共子序列问题。

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战


前言


前面我们学习了动态规划的背包类型问题,其中涉及01背包,完全背包,多重背包。现在我们来继续学习动态规划的公共子序列问题。


公共子序列


我们直接通过leetcode真题来学习这类题型


当然,我们延续之前的动态规划问题的解题步骤


  1. 定义DP数组及下标含义

  2. 推导递推公式

  3. 初始化DP数组

  4. 遍历生成DP数组

最长公共子序列(LCS)

leetcode-114362.png


  1. DP数组及下标

动态规划中公共子序列的问题对于DP数组其实是有套路的,我们一般将DP数组设置为

const dp = [] // dp[i][j] 表示text1取值[0,第i个元素] text2取值[0,第j个元素]
复制代码
  1. 推导递归公式

题目中说公共子序列的长度为text1和text2的相同字符决定,那我们分两种情况讨论

  • 当text1的第i个元素等于text2的第j个元素,那么dp[i][j]可由dp[i - 1][j - 1] + 1得到

有些同学可能会困惑为什么对比的是text1[i - 1]text2[j - 1],那是因为我们说的第i个元素实际对应于text的i-1位置的值。例如第1个元素,实际取值应是text[0]

if (text1[i - 1] === text2[j - 1]) {
  dp[i][j] = dp[i - 1][j - 1]
}
复制代码
  • 当text1的第i个元素不等于text2的第j个元素,那么有可能text1[i - 1]和text2的末尾值匹配,或者是text2[j - 1]和text1的末尾值匹配,所以我们取dp[i][j - 1]和dp[i - 1][j]两者的最大值

有些同学也许会困惑,为什么不可能它们两个同时被匹配呢?其原因是,如果text1[i - 1]和text2[j - 1]匹配,那么text[i - 1]的末尾值已经更新为text1[i],这时依据它们不相等的假设,text2[j]就不可能再和text1的末尾值text1[i]匹配了

if (text1[i - 1] !== text2[j - 1]) {
  dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]
}
复制代码
  1. 初始化DP数组

依据题意可知,当text1或text2为空的时候,肯定是没有公共子序列的,此时对应的DP为0

dp[i][0] = 0
dp[0][j] = 0
复制代码
  1. 遍历生成DP数组

对于text1和text2的遍历顺序其实并不讲究,内外层循环顺序可以调换

// 双层循环
// 外层text1
for (let i = 0; i <= m; i++) {
  // 内层text2
  for (let j = 0; j <= n; j++) {
    //...
  }
}
复制代码
  1. 完整代码
var longestCommonSubsequence = function(text1, text2) {
  const dp = [] // dp[i][j] 表示text1取值[0,i] text2取值[0,j]
  const m = text1.length
  const n = text2.length
  // 双层循环
  // 外层text1
  for (let i = 0; i <= m; i++) {
    // 内层text2
    for (let j = 0; j <= n; j++) {
      // dp初始化 dp[i][0] = 0
      if (j === 0) {
        dp[i] = [0]
        continue
      }
      // dp初始化 dp[0][j] = 0
      if (i === 0) {
        dp[i][j] = 0
        continue
      }
      // 递推公式 分情况讨论
      // 要注意dp[i][j]对应的字符取值[i - 1]和[j - 1]
      if (text1[i - 1] === text2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
      }
    }
  }
  return dp[m][n]
};
复制代码

两个字符串的删除操作

leetcode-58363.png


本题和上题很相似,上题求的是最长公共子串,本题是求最少删除子串,实际我们可以将本题转化为上题。


最少删除字符数 = (字符串1长度 - 最长公共子串) + (字符串2长度 - 最长公共子串)


但是我们这边依然从正面解决该问题,直接求得最少删除字符数


  1. DP数组及下标


套用公共子序列模版

const dp = [] // dp[i][j]表示word1的取值为[0,第i个元素]word2的取值为[0,第j个元素]
复制代码

  1. 推导递归公式


依据题意可知最少删除数实际由两个字符串的最大相同字符决定,只有两个字符相同时才

不用将其删除,所以我们依然分为两种情况讨论


  • 当word1的第i个元素等于word2的第j个元素,那么我们可以不用将其删除dp[i][j]===dp[i - 1][j - 1]

同样的我们要注意第i个元素对应字符串位置为[i - 1],第j个元素对应字符串位置为j-1

if (word1[i - 1] === word2[j - 1]) {
  dp[i][j] = dp[i - 1][j - 1]
}
复制代码

  • 当word1的第i个元素和word2的第j个元素不相同,那么有可能word1[i - 1]和word2末尾元素匹配,或者word2[j - 1]和word1末尾元素匹配,所以我们取dp[i - 1][j]和dp[i][j - 1]两者的最小值+1,+1是因为要加上另外一个剩余的单独字符。

同理,在word1[i - 1]和word2[j - 1]不相等的情况下,它们不可能都被匹配。理由和上题相同。

有的同学还会疑惑dp[i - 1][j - 1]有没有可能小于dp[i - 1][j] + 1?实际不会,dp[i - 1][j]相对dp[i - 1][j - 1]多一个字符,最多可以多消除两个字符,但是我们要加上另外的剩余字符,所以最理想情况是+1-2+1=0,此时dp[i - 1][j - 1] === dp[i - 1][j] + 1

if (word1[i - 1] !== word2[j - 1]) {
  dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1
}
复制代码

  1. 初始化DP数组


依题意可得,当其中一个字符串为空的时候,需要删除的长度为另一字符差长度

dp[i][0] = i
dp[0][j] = j
复制代码

  1. 遍历生成DP数组


同样对于word1和word2的遍历顺序没有要求

// 双层循环
// 外层word1
for (let i = 0; i <= m; i++) {
  // 内层word2
  for (let j = 0; j <= n; j++) {
    //...
  }
}
复制代码

  1. 完整代码
var minDistance = function(word1, word2) {
  const dp = [] // dp[i][j]表示word1的取值为[0,第i个元素]word2的取值为[0,第j个元素]
  const m = word1.length
  const n = word2.length
  // 双层循环
  // 外层遍历word1
  for (let i = 0; i <= m; i++) {
    // 内层遍历word2
    for (let j = 0; j <= n; j++) {
      // dp数组初始化
      if (j === 0) {
        dp[i] = [i]
        continue
      }
      // dp数组初始化
      if (i === 0) {
        dp[i][j] = j
        continue
      }
      // 递推公式
      if (word1[i - 1] === word2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1]
      } else {
        dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + 1
      }
    }
  }
  return dp[m][n]
};
复制代码


结语


本篇文章通过两道leetcode题讲解了动态规划中的公共子序列题型,后面我们继续通过具体问题来讲解单字符串中的动态规划子序列问题。



相关文章
|
1月前
|
存储
【动态规划】子数组系列
本文介绍了多个动态规划问题及其解决方案,包括最大子数组和、环形子数组的最大和、乘积最大子数组、乘积为正数的最长子数组长度、等差数列划分、最长湍流子数组、单词拆分及环绕字符串中唯一的子字符串。通过详细的状态定义、转移方程和代码实现,帮助读者理解每类问题的核心思路与解题技巧。
48 2
|
算法 JavaScript Go
【动态规划】最长递增子序列
【动态规划】最长递增子序列
|
算法
【学会动态规划】 最长递增子序列(26)
【学会动态规划】 最长递增子序列(26)
42 0
深入理解动态规划算法 | 最长公共子序列LCS
深入理解动态规划算法 | 最长公共子序列LCS
126 0
深入理解动态规划算法 - 最长公共子序列
深入理解动态规划算法 - 最长公共子序列
75 0
|
算法 Java C++
数据结构与算法之最长公共子序列&&动态规划
数据结构与算法之最长公共子序列&&动态规划
110 0
数据结构与算法之最长公共子序列&&动态规划
力扣1143. 最长公共子序列 动态规划之最长公共子序列
力扣1143. 最长公共子序列 动态规划之最长公共子序列
190 0
力扣1143. 最长公共子序列 动态规划之最长公共子序列
|
存储 人工智能 算法
『动态规划』最长上升子序列
输入母串的长度 循环输入母串数组以及母串的状态数组并初始化 外层循环,从左往右遍历,记录待更新数组为a[i] 里层循环,遍历母串的左闭右开区间[0,i),找到比a[i]小且状态值最大的数,更新a[i]的状态数组b[i] 用一个变量max记录状态数组b[i]的最大值就是最大子序列的数量
147 0