动态规划: 字符串系列(上)

动态规划: 字符串系列(上)

序言:
本文记录用动态规划解决的常见字符串问题。

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 i1 结尾的子串,与 t t t 中以下标 j − 1 j - 1 j1 结尾的子串最长公共子串的长度为 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[i1][j1]+1,0,s[i1]=t[j1]otherwise
我们定义的 d p [ i ] [ j ] dp[i][j] dp[i][j]很关键,其“定死”了 s s s 必须取到 s [ i − 1 ] s[i - 1] s[i1]字符以及 t t t 必须取到 t [ j − 1 ] t[j - 1] t[j1] 字符,倘若两者不等,则绝无法匹配;倘若两者相等,则可以向上个状态 d p [ i − 1 ] [ j − 1 ] dp[i - 1][j - 1] dp[i1][j1] 转移而来。

  • 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 i1 结尾的子序列,与 t t t 中以下标 j − 1 j - 1 j1 结尾的子序列的最长公共子序列的长度为 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} &amp;dp[i - 1][j - 1] + 1,&amp; \quad{s[i - 1] = t[j -1]}\\ &amp;{\rm max}(dp[i-1][j], dp[i][j -1]), &amp;\rm otherwise \end{aligned} \right. dp[i][j]={dp[i1][j1]+1,max(dp[i1][j],dp[i][j1]),s[i1]=t[j1]otherwise
子序列相较于子串的特殊在于, s s s 中以下标 i − 1 i - 1 i1 结尾的子序列其本身不一定必须取到 s [ i − 1 ] s[i - 1] s[i1], 因此转移方程也变得更加宽松。

  • 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 i1 结尾的子串,与 t t t 中以下标 j − 1 j - 1 j1 结尾的子串的最小距离为 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} &amp;t_1 = 1 + {\rm min}(dp[i][j - 1], dp[i - 1][j]) \\ &amp;t_2 = \left\{ \begin{aligned} &amp;dp[i - 1][j - 1],&amp; \quad{s[i - 1] = t[j -1]}\\ &amp;dp[i - 1][j - 1] + 1, &amp;\rm otherwise \end{aligned} \right. \\ &amp;dp={\rm min}(t_1, t_2) \end{aligned} t1=1+min(dp[i][j1],dp[i1][j])t2={dp[i1][j1],dp[i1][j1]+1,s[i1]=t[j1]otherwisedp=min(t1,t2)
具体而言, d p [ i ] [ j ] dp[i][j] dp[i][j] 有三种转移方式

  1. d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i1][j] 转移到 d p [ i ] [ j ] dp[i][j] dp[i][j],即从 s s s 中多取向后一个字符,此时对 s s s 动用“删除”操作,或者说对 t t t 动用“插入”操作
  2. d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j1] 转移到 d p [ i ] [ j ] dp[i][j] dp[i][j],即从 t t t 中多取向后一个字符,此时对 t t t 动用“删除”操作,或者说对 s s s 动用“插入”操作
  3. d p [ i − 1 ] [ j − 1 ] dp[i - 1][j-1] dp[i1][j1] 转移到 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/动态规划/字符串类型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值