[路飞]_每天刷leetcode_04(最长公共子序列)

641 阅读6分钟

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

经典动态规划:最长公共子序列

最长公共子序列(Longest Common Subsequence, 简称LCS)是一道经典的可以用二维动态规划来解决的问题,大部分比较困难的字符串问题都和这个问题一个套路。而且这个算法稍加改造就可以用于解决其他问题,所以说LCS算法是值得掌握的。

​ --labuladong的算法小抄

leetcode原题传送门最长公共子序列

题目

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

Given two strings text1 and text2, return the length of their longest common subsequence. If there is no common subsequence, return 0.

A subsequence of a string is a new string generated from the original string with some characters (can be none) deleted without changing the relative order of the remaining characters.

For example, "ace" is a subsequence of "abcde". A common subsequence of two strings is a subsequence that is common to both strings.

Example

Input: text1 = "abcde", text2 = "ace" 
Output: 3  
Explanation: The longest common subsequence is "ace" and its length is 3.

Input: text1 = "abc", text2 = "abc"
Output: 3
Explanation: The longest common subsequence is "abc" and its length is 3.

Input: text1 = "abc", text2 = "def"
Output: 0
Explanation: There is no such common subsequence, so the result is 0.

约束条件(Constraints ):

  • 1 <= text1.length, text2.length <= 1000
  • text1 and text2 consist of only lowercase English characters.

思考分割线


解题

这道题,我知道是关于动态规划的问题,但是我是没有做出来的,是看了官网的解释,又查了些资料才有了这篇刷题文章。

首先我们来简单的介绍一下动态规划问题。

什么是动态规划?

动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。20世纪50年代初,美国数学家贝尔曼(R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,从而创立了动态规划。动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域,并在背包问题、生产经营问题、资金管理问题、资源分配问题最短路径问题和复杂系统可靠性问题等中取得了显著的效果

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式

-- 百度百科

嗯,上面说的很宽泛,我们找一个简单版的解释来看一下

How should I explain dynamic programming to a 4-year-old?

writes down "1+1+1+1+1+1+1+1 =" on a sheet of paper

"What's that equal to?"

counting "Eight!"

writes down another "1+" on the left

"What about that?"

quickly "Nine!"

"How'd you know it was nine so fast?"

"You just added one more"

"So you didn't need to recount because you remembered there were eight! Dynamic Programming is just a fancy way to say 'remembering stuff to save time later'"

相信看到这里,读者朋友们对动态规划都有了一个大概的了解, 那么我们怎么解决动态规划问题呢?

动态规划的基本步骤为

  1. 找到“状态”和“选择”
  2. 明确dp数组/函数的定义
  3. 寻找“状态”之间的关系

在实际的算法问题中,写出状态转移方程是最困难的,labuladong总结了关于写出正确的状态转移方程几个思考点

  1. 这个问题的 base case(最简单情况)是什么?
  2. 这个问题有什么“状态”?
  3. 对于每个“状态”,可以做出什么“选择”使得“状态”发生改变?
  4. 如何定义dp数组/函数的含义来表现“状态”和“选择”?

说白了就是三点:状态、选择、dp数组的定义。

好了有了以上的思想武器,我们接下来看这道题具体该如何解决

  • 第一步,确定dp数组

    对于两个字符串的动态规划问题,因为需要比较两个字符串来确定结果,我们一般都需要创建一个二维数组。 image.png 其中dp[i][j]的含义是:对于text1[0..i-1]text2[0..j-1],它们的LCS长度是`dp[i][j];

  • 第二步 定义 base case 索引为0的行和列表示空串,都应该初始化为0,这就是base case.

  • 第三步 找状态转移方程 当 i>0i>0 且 j>0j>0 时,考虑dp[i][j] 的计算: 当text1[i-1] = text2[j-1]时,dp[i][j]= dp[i-1][j-1] + 1text1[i-1] != text2[j-1]时,dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]

经过上面三步,假设text1和text2的长度分别为m和n, 我们最终能计算得到dp[m][n]即为最长公共子序列。

代码实现

var longestCommonSubsequence = function(text1, text2) {
    const m = text1.length, n = text2.length;
    const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
    for (let i = 1; i <= m; i++) {
        const c1 = text1[i - 1];
        for (let j = 1; j <= n; j++) {
            const c2 = text2[j - 1];
            if (c1 === c2) {
                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];
};

好了,这就是我们今天的算法刷题。动态规划问题最难的部分是如何想出转移方程,这个需要我们多多练习。bye, 明天见。

参考资料

如何理解动态规划

动态规划