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

159 阅读4分钟

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

前言

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

公共子序列

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

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

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

  2. 推导递推公式

  3. 初始化DP数组

  4. 遍历生成DP数组

最长公共子序列(LCS)

leetcode-1143

屏幕快照 2022-01-22 下午12.37.06.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-583

屏幕快照 2022-01-22 下午1.32.10.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题讲解了动态规划中的公共子序列题型,后面我们继续通过具体问题来讲解单字符串中的动态规划子序列问题。