序言:
本文记录用动态规划解决的常见字符串问题。
0. 概述
动态规划(Dynamic Programming, DP)是典型地以空间换时间的算法,当暴力法(Brute Force, BF)无法在规定时间内解决问题时,动态规划便能体现出其强大的作用。其主要思路是,当原问题可以被分解成多个子问题,且子问题与原问题拥有重叠的结构,我们可以用多个子问题的解递推出原问题的解(非官话,只为理解)。
1. 最长公共子串(LintCode 79)
问题描述:给出两个字符串,找到最长公共子串,并返回其长度。
输入: s = “ABCD”, t = “EABDF”
输出: 2
解释: s 和 t 的最长公共子串为 “AB”
假定 s s s 的长度为 n n n, t t t 的长度为 m m m。
首先考虑暴力破解, s s s 有 n 2 n^2 n2 (实际是 n ( n + 1 ) / 2 + 1 n(n+1)/2+1 n(n+1)/2+1,此处近似) 个子串, t t t 有 m 2 m^2 m2 个子串,复杂度为 O ( n 2 m 2 ) O(n^2m^2) O(n2m2)
但是暴力破解完全割裂了各个子问题的相互联系性。
倘若记 s s s 中以下标 i − 1 i - 1 i−1 结尾的子串,与 t t t 中以下标 j − 1 j - 1 j−1 结尾的子串最长公共子串的长度为 d p [ i ] [ j ] dp[i][j] dp[i][j],则状态转移方程为
d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] + 1 , s [ i − 1 ] = t [ j − 1 ] 0 , o t h e r w i s e dp[i][j]=\left\{ \begin{aligned} &dp[i - 1][j - 1] + 1,& \quad{s[i - 1] = t[j -1]}\\ &0,& \rm otherwise \end{aligned} \right. dp[i][j]={dp[i−1][j−1]+1,0,s[i−1]=t[j−1]otherwise
我们定义的 d p [ i ] [ j ] dp[i][j] dp[i][j]很关键,其“定死”了 s s s 必须取到 s [ i − 1 ] s[i - 1] s[i−1]字符以及 t t t 必须取到 t [ j − 1 ] t[j - 1] t[j−1] 字符,倘若两者不等,则绝无法匹配;倘若两者相等,则可以向上个状态 d p [ i − 1 ] [ j − 1 ] dp[i - 1][j - 1] dp[i−1][j−1] 转移而来。
class Solution {
public:
/**
* @param A: A string
* @param B: A string
* @return: the length of the longest common substring.
*/
int longestCommonSubstring(string &A, string &B) {
int len1 = A.size(), len2 = B.size();
int ans = 0;
vector > dp(len1 + 1, vector(len2 + 1, 0));
for (int i = 1; i <= len1; ++i) {
for (int j = 1; j <= len2; ++j) {
if (A[i - 1] == B[j - 1]) {
dp[i][j] = 1 + dp[i - 1][j - 1];
ans = max(ans, dp[i][j]);
}
}
}
return ans;
}
};
2. 最长公共子序列(LintCode 77)
问题描述:给出两个字符串,找到最长公共子序列(LCS),返回LCS的长度。
输入: s = “ABCD”, t = “EABDF”
输出: 3
解释: s 和 t 的最长公共子序列为 “ABD”
不同于子串,子序列的定义更加宽松。对于字符串 s s s, 子串要求从 s s s 中顺序取出,并且严格相邻;而子序列则只要求顺序取出,而不一定相邻(可以不连续)
子序列的定义等于直接宣告暴力破解的失败,但是对于动态规划而言,却无足轻重。可以依旧挪用上题的 d p dp dp 定义,记 s s s 中以下标 i − 1 i - 1 i−1 结尾的子序列,与 t t t 中以下标 j − 1 j - 1 j−1 结尾的子序列的最长公共子序列的长度为 d p [ i ] [ j ] dp[i][j] dp[i][j]。因为,子串也是子序列,因此,只要拓展状态转移方程即可。
d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] + 1 , s [ i − 1 ] = t [ j − 1 ] m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) , o t h e r w i s e dp[i][j]=\left\{ \begin{aligned} &dp[i - 1][j - 1] + 1,& \quad{s[i - 1] = t[j -1]}\\ &{\rm max}(dp[i-1][j], dp[i][j -1]), &\rm otherwise \end{aligned} \right. dp[i][j]={dp[i−1][j−1]+1,max(dp[i−1][j],dp[i][j−1]),s[i−1]=t[j−1]otherwise
子序列相较于子串的特殊在于, s s s 中以下标 i − 1 i - 1 i−1 结尾的子序列其本身不一定必须取到 s [ i − 1 ] s[i - 1] s[i−1], 因此转移方程也变得更加宽松。
class Solution {
public:
/**
* @param A: A string
* @param B: A string
* @return: The length of longest common subsequence of A and B
*/
int longestCommonSubsequence(string &A, string &B) {
int len1 = A.size(), len2 = B.size();
vector > dp(len1 + 1, vector(len2 + 1, 0));
int ans = 0;
for (int i = 1; i <= len1; ++i) {
for (int j = 1; j <= len2; ++j) {
if (A[i - 1] == B[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
ans = max(ans, dp[i][j]);
}
}
return ans;
}
};
3.字符串相似度/编辑距离(LeetCode 72)
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
输入: word1 = “horse”, word2 = “ros”
输出: 3
解释:
horse -> rorse (将’h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’) rose -> ros (删除 ‘e’)
仍旧沿用“结尾”法定义 d p dp dp, 记 s s s 中以下标 i − 1 i - 1 i−1 结尾的子串,与 t t t 中以下标 j − 1 j - 1 j−1 结尾的子串的最小距离为 d p [ i ] [ j ] dp[i][j] dp[i][j],则
t 1 = 1 + m i n ( d p [ i ] [ j − 1 ] , d p [ i − 1 ] [ j ] ) t 2 = { d p [ i − 1 ] [ j − 1 ] , s [ i − 1 ] = t [ j − 1 ] d p [ i − 1 ] [ j − 1 ] + 1 , o t h e r w i s e d p = m i n ( t 1 , t 2 ) \begin{aligned} &t_1 = 1 + {\rm min}(dp[i][j - 1], dp[i - 1][j]) \\ &t_2 = \left\{ \begin{aligned} &dp[i - 1][j - 1],& \quad{s[i - 1] = t[j -1]}\\ &dp[i - 1][j - 1] + 1, &\rm otherwise \end{aligned} \right. \\ &dp={\rm min}(t_1, t_2) \end{aligned} t1=1+min(dp[i][j−1],dp[i−1][j])t2={dp[i−1][j−1],dp[i−1][j−1]+1,s[i−1]=t[j−1]otherwisedp=min(t1,t2)
具体而言, d p [ i ] [ j ] dp[i][j] dp[i][j] 有三种转移方式
class Solution {
public:
int minDistance(string word1, string word2) {
int len1 = word1.size(), len2 = word2.size();
if (len1 == 0 || len2 == 0) return len1 + len2;
vector> dp(len1 + 1, vector(len2 + 1, 0));
for (int i = 1; i <= len1; ++i) dp[i][0] = i;
for (int j = 1; j <= len2; ++j) dp[0][j] = j;
for (int i = 1; i <= len1; ++i) {
for (int j = 1; j <= len2; ++j) {
int tar1 = 1 + min(dp[i][j - 1], dp[i - 1][j]);
int tar2 = word1[i - 1] == word2[j - 1] ? dp[i - 1][j - 1] : dp[i - 1][j - 1] + 1;
dp[i][j] = min(tar1, tar2);
}
}
return dp[len1][len2];
}
};
ajaxlt的GitHub入口: https://github.com/ajaxlt/BasicAlgorithoms/tree/master/动态规划/字符串类型