题目大意

  • 给你两个字符串,需要你通过执行三种操作,将一个字符串修改为另一个字符串。但是你执行这三种操作都是有代价的。三种操作分别对应三种代价。你需要使用最少的代价将这个字符串修改为另一个字符串。

思路分析

  • 首先,这个题目是比较难的,我来大致介绍一下我的思路和想法。对于这个问题,最开始我想的是利用DP进行求解。但是,我定义的dp方程是dp[5010][3],也就是分别对应每个位置执行三种操作的最小的代价。但是这里我进行推导的时候发现这个是有问题的。在这个状态的定义中,我们无法进行状态的转移。而且这样定义的dp是没有后效性的。所以,我们需要想其他办法。

  • 然后,我发现,其实这个可以和最长公共子序列的题目相联系(LCS),我们定义dp[i][j]分别表示字符串str1的前i个字符和字符串str2的前j个字符完全匹配之后的最小的代价。然后,我们来看一下这个是否有合法的状态转移。当我们的状态到达i,j的时候,我们确保了(i-1,j),(i,j-1)和(i-1,j-1)这些都是已经知道了的。那么根据题目意思,我们想要把第一个字符串转换为另一个字符串。我们需要执行三种操作,当然,我们并不知道执行哪种操作是最好的。于是我们枚举存储出三种操作的情况,假设在str1这个位置执行删除操作。那么为了保持str1[0...i]和str2[0...j]完全匹配的话,就可以由str1[0...i-1]和str2[0...j]完全匹配的情况转移过来。也就是,同理,假如执行插入操作,那么为了保持str1[0...i]和str2[0...j]完全匹配的话,就可以由str1[0...i]和str2[0...j-1]完全匹配的情况转移过来。也就是;执行替换操作同理可以得到。这三者取最小的即可。所以,这种状态方程的定义是存在后效性的。接下来,我们就可以码代码了。但是在码代码之前我们需要初始化一些条件,为了实现状态的顺利转移,我们肯定要定义状态的起点,这里状态的起点就是当两个字符串其中一个长度是0的时候,我们先进行预处理。然后就可以顺利地转移啦!

  • 请结合下图进行理解

图片说明

解法一 普通DP

  • 这种方法地思路如上,但是其实还可以优化地,具体请看下一种解法。

  • 代码如下

    • 我们需要循环枚举两个字符串当前地位置来表示当前地状态,所以时间复杂度为O(nm)
    • 开了一个二维数组存储每个状态,空间复杂度为O(nm)
class Solution {
public:
    /**
     * min edit cost
     * @param str1 string字符串 the string
     * @param str2 string字符串 the string
     * @param ic int整型 insert cost
     * @param dc int整型 delete cost
     * @param rc int整型 replace cost
     * @return int整型
     */
    // dp[i][j]表示的是str1的前i个字符和str2的前j个字符完全匹配的所需要的最小的花费
    int dp[5010][5010];
    int minEditCost(string str1, string str2, int ic, int dc, int rc) {
        // write code here
        int n=str1.size();
        int m=str2.size();
        for(int i=1;i<=n;i++){
            dp[i][0]=i*dc;
        }
        for(int i=1;i<=m;i++){
            dp[0][i]=i*ic;
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                if(str1[i-1]==str2[j-1]){
                    // 此时匹配了,那么不进行操作
                    dp[i][j]=dp[i-1][j-1];
                }else{
                    // 此时字符不匹配,执行三种操作中最优的那种
                    int ins=dp[i][j-1]+ic;
                     // 在这个位置插入一个字符,使得[0,i]和[0,j-1]合法的情况下,str1[i+1]==str2[j]
                    int del=dp[i-1][j]+dc;
                    // 在这个位置删除一个字符,使得[0,i-1]字符和[0,j]字符合法
                    int rep=dp[i-1][j-1]+rc;
                    // 替换掉这个字符,就是从[0,i-1]和[0,j-1]进行转移 
                    dp[i][j]=min(ins,min(del,rep));  // 取最小的
                }
            }
        }
        return dp[n][m];
    }
};

解法二 DP空间优化

  • 在上面那种解法我们可以看到,对于状态方程地转移虽然没有问题,但是我们的数组开了一个二维的很大的。但是在进行状态的转移的时候,我们可以发现,其实只用了这个状态的上一个状态。而没有用之前更早的状态。学过DP算法的一般都知道,这种情况下一般是可以利用滚动数组优化掉一维空间的。那么我们就可以看看怎么优化这一维呢?首先,我们可以确实我们可以优化的是外面那一维状态。这个可以根据第一种解法的状态转移方程得出,我们重新定义状态方程dp[i]表示的是在当前的str1的情况下,字符串str2的前i个字符需要和str1进行匹配的最小的代价。同样,对于之前的哪三种情况也是同样的进行转移。但是这里要注意的是我们我们为了用到上一个状态,我们需要使用一个pre变量临时存储每个状态的上一个状态的最小代价。具体请看代码实现。
  • 代码如下
    • 还是需要遍历字符串的所有位置进行状态转移,时间复杂度为O(nm)
    • 这种方法优化了一维,只记录了str2的状态,空间复杂度为O(n)。
class Solution {
public:
    /**
     * min edit cost
     * @param str1 string字符串 the string
     * @param str2 string字符串 the string
     * @param ic int整型 insert cost
     * @param dc int整型 delete cost
     * @param rc int整型 replace cost
     * @return int整型
     */
    int dp[5010];  // dp[i]表示对于此时的str1,需要满足和str2的前i个字符相等的最小代价
    int minEditCost(string str1, string str2, int ic, int dc, int rc) {
        // write code here
        int n=str1.size(),m=str2.size();
        for(int i=0;i<=m;i++){
            // 此时str1的长度为0的情况下,我们需要求出str1和str2的前i个字符匹配的最小代价
            dp[i]=i*ic;
        }
        for(int i=1;i<=n;i++){
            int pre=dp[0]; // 提前记录此时str2的前0个字符需要匹配的最小代价,pre用来存储前一个状态的最小代价
            dp[0]=i*dc;
            for(int j=1;j<=m;j++){
                // 此时在str1字符串长度为i的情况下,str2的前j个字符要完全匹配的最小的代价
                int ins=dp[j-1]+ic;
                int del=dp[j]+dc;
                int rep=pre+(str1[i-1]==str2[j-1]?0:rc);
                pre=dp[j]; // 及时更新,给下一个状态使用
                dp[j]=min(ins,min(del,rep));
            }
        }
        return dp[m];
    }
};