目录
前言
和单序列问题不同,双序列问题的输入有两个或更多的序列,通常是两个字符串或数组。由于输入是两个序列,因此状态转移方程通常有两个参数,即 f(i, j),定义第 1 个序列中下标从 0 到 i 的子序列和第 2 个序列中下标从 0 到 j 的子序列的最优解(或解的个数)。一旦找到了 f(i, j) 与 f(i - 1, j - 1)、f(i - 1, j) 和 f(i, j - 1) 的关系,通常问题也就迎刃而解。
由于双序列的状态转移方程有两个参数,因此通常需要使用一个二维数组来保存状态转移方程的计算结果。但在大多数情况下,可以优化代码的空间效率,只需要保存二维数组的一行就可以完成状态转移方程的计算,因此可以只用一个一维数组就能实现二维数组的缓存功能。
接下来通过几个典型的编程题目来介绍如何应用动态规划解决双序列问题。
面试题 95 : 最长公共子序列
题目:
输入两个字符串,请求出它们的最长公共子序列的长度。如果从字符串 s1 中删除若干字符之后能得到 s2,那么字符串 s2 就是字符串 s1 的一个子序列。例如,从字符串 "abcde" 中删除两个字符之后能得到字符串 "ace",因此字符串 "ace" 是字符串 "abcde" 的一个子序列。但字符串 "aec" 不是字符串 "abcde" 的子序列。如果输入字符串 "abcde" 和 "badfe",那么它们的最长公共子序列是 "bde",因此输出 3。
分析:
两个字符串可能存在多个公共子序列,如果空字符串 ""、"a"、"ad" 与 "bde" 等都是字符串 "abcde" 和 "badfe" 的公共子序列。这个题目没有要求列出两个字符串的所有公共子序列,而只是计算最长公共子序列的长度,也就是求问题的最优解,因此可以考虑应用动态规划来解决这个问题。
分析确定状态转移方程:
应用动态规划解决问题的关键在于确定状态转移方程。由于输入有两个字符串,因此状态转移方程有两个参数。用函数 f(i, j) 表示第 1 个字符串中下标从 0 到 i 的子字符串(记为 s1[0···i])和第 2 个字符串中下标从 0 到 j 的子字符串(记为 s2[0···j])的最长公共子序列的长度。如果第 1 个字符串的长度是 m,第 2 个字符串的长度是 n,那么 f(m - 1, n - 1) 就是整个问题的解。
如果第 1 个字符串中下标为 i 的字符(记为 s1[i])与第 2 个字符串中下标为 j 的字符(记为 s2[j])相同,那么 f(i, j) 相当于在 s1[0···i-1] 和 s2[0···j-1] 的最长公共子序列的后面添加一个公共字符,也就是 f(i, j) = f(i - 1, j - 1) + 1。
如果字符 s1[i] 与字符 s2[j] 不相同,则这两个字符不可能同时出现在 s1[0···i] 和 s2[0···j] 的公共子序列中。此时 s1[0···i] 和 s2[0···j] 的最长公共子序列要么是 s1[0···i-1] 和 s2[0···j] 的最长公共子序列,要么是 s1[0···i] 和 s2[0···j-1] 的最长公共子序列。也就是说,此时 f(i, j) 是 f(i - 1, j) 和 f(i, j - 1) 的最大值。
可以将这个问题的转移转移方程总结为:
当上述状态转移方程的 i 或 j 等于 0 时,即求 f(0, j) 或 f(i, 0) 时可能需要 f(-1, j) 或 f(i, -1) 的值。f(0, j) 的含义是 s1[0···0] 和 s2[0···j] 这两个子字符串的最长公共子序列的长度,即第 1 个子字符串只包含一个下标为 0 的字符,那么 f(-1, j) 对应的第 1 个字符串再减少一个字符,所以第 1 个子字符串是空字符串。任意空字符串和另一个字符串的公共子序列的长度都是 0,所以 f(-1, j) 的值等于 0。同理,f(i, -1) 的值也等于 0。
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size(), n = text2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 0; i < m; ++i)
{
for (int j = 0; j < n; ++j)
{
if (text1[i] == text2[j])
dp[i + 1][j + 1] = dp[i][j] + 1;
else
dp[i + 1][j + 1] = max(dp[i][j + 1], dp[i + 1][j]);
}
}
return dp[m][n];
}
};
由于表格中有 i 等于 -1 对应的行和 j 等于 -1 对应的列,因此如果输入字符串的长度分别为 m、n,那么代码中的二维数组 dp 的行数和列数分别是 m + 1 和 n + 1。f(i, j) 的值保存在 dp[i + 1][j + 1] 中。
这种解法的空间复杂度和时间复杂度都是 O(mn)。
优化空间效率,只保存表格中的两行:
需要注意的是,f(i, j) 的值依赖于表格中左上角 f(i - 1, j - 1) 的值、正上方 f(i - 1, j) 的值和同一行左边 f(i, j - 1) 的值。由于计算 f(i, j) 的值时只需要使用上方一行的值和同一行左边的值,因此实际上只需要保存表格中的两行就可以。
class Solution {
public:
int longest