动态规划: 字符串系列(上)
序言:
本文记录用动态规划解决的常见字符串问题。
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] 转移而来。
- C++ 实现,时间复杂度 O ( n m ) O(nm) O(nm)
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<vector<int> > dp(len1 + 1, vector<int>(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], 因此转移方程也变得更加宽松。
- C++ 实现,时间复杂度 O ( n m ) O(nm) O(nm)
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<vector<int> > dp(len1 + 1, vector<int>(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] 有三种转移方式
- d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i−1][j] 转移到 d p [ i ] [ j ] dp[i][j] dp[i][j],即从 s s s 中多取向后一个字符,此时对 s s s 动用“删除”操作,或者说对 t t t 动用“插入”操作
- d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j−1] 转移到 d p [ i ] [ j ] dp[i][j] dp[i][j],即从 t t t 中多取向后一个字符,此时对 t t t 动用“删除”操作,或者说对 s s s 动用“插入”操作
- d p [ i − 1 ] [ j − 1 ] dp[i - 1][j-1] dp[i−1][j−1] 转移到 d p [ i ] [ j ] dp[i][j] dp[i][j], 即分别从 即从 s s s, t t t 中多向后取一个字符。若取出的两个字符相等, 无需操作;反之,动用“替换”操作。
- C++ 实现, 时间复杂度 O ( n m ) O(nm) O(nm)
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<vector<int>> dp(len1 + 1, vector<int>(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/动态规划/字符串类型