动态规划之编辑距离问题

138 阅读3分钟

一、392. 判断子序列 - 力扣(LeetCode)

题目要求: 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

这道题双指针也可以做,但是在这里利用这道题作为入门题目来学习动态规划里的编辑距离问题。

重点是想清楚dp数组的含义:dp[i] [j] 表示以下标i为结尾的字符串s,和以下标j为结尾的字符串t,相同子序列的长度为dp[i] [j]

在确定递推公式的时候,要考虑如下两种情况:

  • if (s[i] == t[j])
    • t中找到了一个字符在s中也出现了
  • if (s[i] != t[j])
    • 相当于t要删除元素,继续匹配

if (s[i] == t[j]),那么dp[i] [j] = dp[i - 1] [j - 1] + 1;,因为找到了一个相同的字符,相同子序列长度自然要在dp[i-1] [j-1]的基础上加1。

if (s[i] != t[j]),**此时相当于t要删除元素,**t如果把当前元素t[j]删除,删除当前t多余的不相等的元素,当前最大的相同子序列长度就是之前相比较得来的值,那么dp[i][j]的数值就是 看以s[i]结尾的子串 与 以t[j - 1]结尾的子串 的比较结果了

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int m = s.size();
        int n = t.size();
        if(m == 0)
            return true;
        if(n == 0)
            return false;
        vector<vector<int>>dp(m,vector<int>(n));//dp[i][j] 表示以下标i为结尾的字符串s,和以下标j为结尾的字符串t,相同子序列的长度为dp[i][j]
        for(int i = 0;i < t.size();i ++)
        {
            if(i > 0 && dp[0][i - 1] == 1)
                dp[0][i] = 1;
            else if(s[0] == t[i])
                dp[0][i] = 1;
        }
        for(int i = 0;i < s.size();i ++)
        {
            if(i > 0 && dp[i - 1][0] == 1)
                dp[i][0] = 1;
            else if(s[i] == t[0])
                dp[i][0] = 1;
        }
        for(int i = 1;i < m;i ++)
        {
            for(int j = 1;j < n;j ++)
            {
                if(t[j] == s[i])
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                else//t[j] != s[i],那就要删除当前t多余的不相等的元素,当前最大的相同子序列长度就是之前相比较得来的值
                    dp[i][j] = dp[i][j - 1];//那么dp[i][j]的数值就是 看以s[i]结尾的子串 与 以t[j - 1]结尾的子串 的比较结果了
            }
        }
        return dp[m - 1][n - 1] == m;
    }
};

二、115. 不同的子序列 - 力扣(LeetCode)

题目要求: 给你两个字符串 st ,统计并返回在 s子序列t 出现的个数。

解析:

1、确定dp数组以及下标的含义

dp[i] [j]:以j为结尾的s子序列中出现以i为结尾的t的个数为dp[i] [j]。

2、确定递推公式

这一类问题,基本是要分析两种情况

  • s[j] 与 t[i]相等
  • s[j] 与 t[i] 不相等

当s[j] 与 t[i]相等时,又要分成两种情况,因为dp[i] [j]可以有两部分组成。

一部分是用s[j]来匹配,那么个数为dp[i - 1] [j - 1]。即不需要考虑当前s子串和t子串的最后一位字母,所以只需要 dp[i-1] [j-1]。

一部分是不用s[j]来匹配,个数为dp[i ] [j - 1]。

这两种情况的数值要加起来!!

当s[j] 与 t[i] 不相等的时候,那么肯定不能用s[j]来匹配字符串t了,那么这个时候dp[i] [j]的值应该就是s[j - 1]去匹配t[i]的结果,那么就是dp[i] [j - 1]。

所以是:

if(t[i] == s[j])
                {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1];
                }else{
                    dp[i][j] = dp[i][j - 1];
                }

完整代码如下:

class Solution {
public:
    int numDistinct(string s, string t) {
        int n = s.size();
        int m = t.size();
        if(m > n)
            return 0;
        vector<vector<unsigned long long>>dp(m,vector<unsigned long long>(n));
        dp[0][0] = s[0] == t[0]?1:0;
        for(int i = 1;i < n;i ++)
        {
            if(t[0] == s[i])
                dp[0][i] = dp[0][i - 1] + 1;
            else
                dp[0][i] = dp[0][i - 1];
        }
        for(int i = 1;i < m;i ++)
        {
            for(int j = i;j < n;j ++)
            {
                if(t[i] == s[j])
                {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1];
                }else{
                    dp[i][j] = dp[i][j - 1];
                }
            }
        }
        return dp[m - 1][n - 1];
    }
};

三、583. 两个字符串的删除操作 - 力扣(LeetCode)

题目要求: 给定两个单词 word1word2 ,返回使得 word1word2 相同所需的最小步数

每步 可以删除任意一个字符串中的一个字符。

解析:

1、确定dp数组(dp table)以及下标的含义

dp[i] [j]:以i为结尾的字符串word1,和以j位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。

本题初始化稍难,好好想想。

2、确定递推公式

  • 当word1[i] 与 word2[j ]相同的时候。

    此时不需要删除,那么就是dp[i - 1] [j - 1]

  • 当word1[i] 与 word2[j]不相同的时候。

    此时有三个来源,取三个来源里面最小的那一个。dp[i - 1] [j - 1]是代表把不相等的这俩元素都删了,所以操作数要加2。dp[i - 1] [j]和dp[i] [j - 1]是代表只删一个,所以操作数要加1。

代码如下:

class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.size();
        int n = word2.size();
        vector<vector<int>>dp(m,vector<int>(n));
        dp[0][0] = word1[0] == word2[0]?0:2;
        for(int i = 1;i < m;i ++)//初始化
        {
            if(word2[0] == word1[i])
                dp[i][0] = i;
            else
                dp[i][0] = dp[i - 1][0] + 1;
        }
        for(int i = 1;i < n;i ++)//初始化
        {
            if(word1[0] == word2[i])
                dp[0][i] = i;
            else
                dp[0][i] = dp[0][i - 1] + 1;
        }
        for(int i = 1;i < m;i ++)
        {
            for(int j = 1;j < n;j ++)
            {
                if(word1[i] == word2[j])
                {
                    dp[i][j] = min({dp[i - 1][j - 1],dp[i - 1][j] + 1,dp[i][j - 1] + 1});//其实这里考虑多了两个方向,dp[i - 1][j] + 1和dp[i][j - 1] + 1应该是一定比dp[i - 1][j - 1]大的
                }else{
                    dp[i][j] = min({dp[i - 1][j - 1] + 2,dp[i - 1][j] + 1,dp[i][j - 1] + 1});
                }
            }
        }
        return dp[m - 1][n - 1];
    }
};

当然,本题其实就是在求最长公共子序列的长度1143. 最长公共子序列 - 力扣(LeetCode)。这里就把最长公共子序列的长度求出来,然后用两个字符串的长度减去公共序列的长度,再加起来就好了。

也可以这么写:

class Solution {
public:
    int minDistance(string word1, string word2) {
        int size1 = word1.size();
        int size2 = word2.size();
        //之前做过求最长公共子序列的问题,这里就把最长公共子序列的长度求出来,然后用两个字符串的长度减去公共序列的长度,再加起来就好了。
        vector<vector<int>>dp(size1 + 1,vector<int>(size2 + 1));//默认初始化都是0
        for(int i = 1;i <= size1;i ++)
        {
            for(int j = 1;j <= size2;j ++)
            {
                if(word1[i - 1] == word2[j - 1])
                {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }else{
                    dp[i][j] = max(dp[i][j - 1],dp[i- 1][j]);
                }
            }
        }
        return size1 + size2 - dp[size1][size2]*2;
    }
};

四、72. 编辑距离 - 力扣(LeetCode)

题目要求: 给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符 删除一个字符 替换一个字符

解析:

这道题和上面一道题相比,就是可以替换了,多了一种操作方式。

至于插入也就是增添字符,就相当于是另一个字符串删除字符,就比如示例:

image.png rose到ros也可以是ros增添e得来,所以增添和删除是对应的,可以一起考虑。

那么这道题和上一题583. 两个字符串的删除操作 - 力扣(LeetCode)就很像了。

区别只体现在初始化和递推公式上面:初始化的时候,由于可以替换,所以dp[0] [0] = word1[0] == word2[0]?0:1;

如果不一样的话可以选择替换,最小操作数是1而不是0了

递推公式: 当word1[i ] 与 word2[j ]相同的时候,dp[i] [j] = dp[i - 1] [j - 1];

当word1[i] 与 word2[j ]不相同的时候,有三种情况:word1删除一个元素,word2删除一个元素,以及 替换掉当前不一样的元素。

dp[i][j] = min({dp[i - 1][j - 1] + 1,dp[i - 1][j] + 1,dp[i][j - 1] + 1});//三种来源,分别是word1删除一个字符,操作数加一,word2删除一个字符,操作数加一,word1和word2替换掉当前不相等的字符,操作数加一

完整代码如下:

class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.size();
        int n = word2.size();
        if(m == 0)
            return n;
        if(n == 0)
            return m;
        vector<vector<int>>dp(m,vector<int>(n));
        dp[0][0] = word1[0] == word2[0]?0:1;//初始化,注意这里,和上一题相比,如果不一样的话可以选择替换,最小操作数是1而不是0了
        //下面的初始化画个图就能明白了
        for(int i = 1;i < m;i ++)//初始化
        {
            if(word2[0] == word1[i])
                dp[i][0] = i;
            else
                dp[i][0] = dp[i - 1][0] + 1;
        }
        for(int i = 1;i < n;i ++)//初始化
        {
            if(word1[0] == word2[i])
                dp[0][i] = i;
            else
                dp[0][i] = dp[0][i - 1] + 1;
        }
        for(int i = 1;i < m;i ++)
        {
            for(int j = 1;j < n;j ++)
            {
                if(word1[i] == word2[j])
                {
                    dp[i][j] = dp[i - 1][j - 1];
                }else{
                    dp[i][j] = min({dp[i - 1][j - 1] + 1,dp[i - 1][j] + 1,dp[i][j - 1] + 1});//三种来源,分别是word1删除一个字符,操作数加一,word2删除一个字符,操作数加一,word1和word2替换掉当前不相等的字符,操作数加一
                }
            }
        }
        return dp[m - 1][n - 1];
    }
};