fucking-algorithm icon indicating copy to clipboard operation
fucking-algorithm copied to clipboard

经典动态规划:编辑距离 :: labuladong的算法小抄

Open utterances-bot opened this issue 3 years ago • 23 comments

文章链接点这里:经典动态规划:编辑距离

评论礼仪 见这里,违者直接拉黑。

utterances-bot avatar Jan 02 '22 07:01 utterances-bot

厉害了

CNLarryAn avatar Jan 02 '22 07:01 CNLarryAn

递归Java版(带备忘录)

class Solution {
    int[][] memo;
    public int minDistance(String word1, String word2) {
        memo = new int[word1.length()][word2.length()];
        return dp(word1, word2, word1.length()-1, word2.length()-1);
    }
    private int dp(String s1, String s2, int i, int j){
        if(i == -1) return j+1;
        if(j == -1) return i+1;
        if(memo[i][j] != 0) return memo[i][j];
        if(s1.charAt(i) == s2.charAt(j)){
            memo[i][j] = dp(s1,s2,i-1,j-1);
        }else{
            int a = dp(s1,s2,i,j-1) + 1;
            int b = dp(s1,s2,i-1,j) + 1;
            int c = dp(s1,s2,i-1,j-1) + 1;
            memo[i][j] = Math.min(a, Math.min(b, c));
        }
        return memo[i][j];
    }
}

deepbreath373 avatar Jan 18 '22 07:01 deepbreath373

DP table版应该再初始一个dp[0][0]=0吧

fornobugworld avatar Feb 05 '22 10:02 fornobugworld

其实插入等价于删除,在字符串1里插入P 等价于在字符串2删除对应的字符。操作只有删除和替换。

Nelsonlin0321 avatar Feb 16 '22 09:02 Nelsonlin0321

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        
        max_distance = max(len(word1),len(word2))
        
        # base case
        if len(word1) * len(word2)==0:
            return max_distance
        
        
        #dp table
        row = [max_distance for _ in range(len(word2)+1)]
        dp = [row.copy() for _ in range(len(word1)+1)]
        
        dp[0][0] = 0
        
        # base case
        for i in range(len(word1)+1):
            dp[i][0] = i
        
        for j in range(len(word2)+1):
            dp[0][j] = j
        
        
        for i in range(1,len(word1)+1):
            for j in range(1,len(word2)+1):
                char_1 = word1[i-1]
                char_2 = word2[j-1]
                
                if char_1 == char_2:
                    # 相同的情况上,就不用操作,就等于上一个阶段
                    dp[i][j] = dp[i-1][j-1]
                else:
                    # 不相同的情况上,有三种操作的可能 1)删除左边 2) 删除右边 3)替换/  插入跟删除本质是同一个种操作
                    dp[i][j] = 1 + min(dp[i][j-1],dp[i-1][j],dp[i-1][j-1])
        
        return dp[-1][-1]

Nelsonlin0321 avatar Feb 16 '22 09:02 Nelsonlin0321

东哥 有个图挂了

JackHo327 avatar Feb 28 '22 07:02 JackHo327

@JackHo327 没有吧,应该是你网络问题

labuladong avatar Mar 03 '22 04:03 labuladong

东哥,第三个大标题上面的大段代码,关于dp数组的定义,是不是不大对?

// 定义:s1[0..i] 和 s2[0..j] 的最小编辑距离是 dp[i-1][j-1]

莫不是应该写成?

// 定义:s1[0..i-1] 和 s2[0..j-1] 的最小编辑距离是 dp[i][j]

MathsGo avatar Mar 11 '22 02:03 MathsGo

@MathsGo 是我写错了,感谢指出~

labuladong avatar Mar 11 '22 12:03 labuladong

class Solution {
public:        
    int memo[501][501];
    int minDistance(string word1, string word2) {
        int m = word1.size();
        int n = word2.size();
        for(int i=1;i<=m;i++)
            for(int j=1;j<=n;j++)
                memo[i][j] = 0;
        return dp(m,n, word1, word2);
    }
    int dp(int i, int j, string word1, string word2){
            if(memo[i][j]!=0)
                return memo[i][j];
            if(i==0) return memo[i][j] = j;
            if(j==0) return memo[i][j] =i;
            if(word1[i-1]==word2[j-1])
                memo[i][j] = dp(i-1,j-1, word1, word2);
            else
                 memo[i][j] = min(min(dp(i-1,j,word1,word2)+1,dp(i,j-1,word1,word2)+1),dp(i-1,j-1,word1,word2)+1);
            return memo[i][j];
    }
};

cmh1779 avatar Mar 20 '22 09:03 cmh1779

#递归+备忘录--python class Solution: def minDistance(self, word1: str, word2: str) -> int:

    help_dict={}
    def dp(i: int, j: int):

        if (i,j) in help_dict:
            return help_dict[(i,j)]
        #base case: word1没走完,word1全删掉,word2没走完,word1全插入
        if i == -1:
            return j+1
        if j == -1:
            return i+1
        
        if word1[i] == word2[j]:
            help_dict[(i,j)] = dp(i-1, j-1)      #本来就匹配的,i,j前移  

        else:
            
            help_dict[(i,j)] =  min(dp(i, j-1)+1,  #插入字符后,匹配了,但i不变,j左移
                                    dp(i-1,j)+1,   #删除字符后,i前移,j不变
                                    dp(i-1, j-1)+1)#替换字符后,匹配了,i前移,j也前移
        return help_dict[(i,j)]
    return dp(len(word1)-1, len(word2)-1)

zy159864 avatar Mar 22 '22 09:03 zy159864

自底向上C++代码

class Solution {
public:
    int minDistance(string word1, string word2) {
        int size1 = word1.size(), size2 = word2.size();
        vector<vector<int>> dp(size1 + 1, vector<int>(size2 + 1));
        //base case
        for(int i = 1; i<= size1; ++i)
            dp[i][0] = i;
        for(int j = 1; j <= size2; ++j)
            dp[0][j] = j;
            //自底向上求解
        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];
                else
                    dp[i][j] = minAbc(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1);
            }
        }
        return dp[size1][size2];
    }

    int minAbc(int a, int b, int c) {
        return min(a, min(b, c));
    }
};

Pierce-Dai avatar Apr 15 '22 12:04 Pierce-Dai

递归解法的复杂度是多少?

SharkLJ avatar Apr 23 '22 23:04 SharkLJ

东哥,递归函数中插入的情况,“我直接在s1[i]插入一个和s2[j]一样的字符",有点不理解; 根据动图,s1中插入新字母加后,s1[i]对应的字母没有变化,是不是这样表述更好“直接在s1[i+1]插入一个...”。

NealCou avatar Apr 24 '22 12:04 NealCou

@NealCou 嗯,你说的有道理,我修改一下表述

labuladong avatar Apr 25 '22 03:04 labuladong

javascript version

// 动态规划-备忘录
var minDistance = function(word1, word2) {
    let m = word1.length;
    let n = word2.length;
    // 备忘录初始化为特殊值 表示还未计算
    let memo = new Array(m).fill(-1).map(() => new Array(n).fill(-1));
    return dp(word1, m - 1, word2, n - 1);   

    function dp(s1, i, s2, j) {
        if (i == -1) return j + 1;
        if (j == -1) return i + 1;
        // 查询备忘录 避免重叠子问题 递归树剪枝
        if (memo[i][j] != -1) {
            return memo[i][j];
        }
        // 状态转移 结果存入备忘录
        if (s1[i] === s2[j]) {
            memo[i][j] = dp(s1, i - 1, s2, j - 1);
        } else {
            memo[i][j] = Math.min(
                dp(s1, i, s2, j - 1) + 1, 
                dp(s1, i - 1, s2, j) + 1, 
                dp(s1, i - 1, s2, j - 1) + 1
            );
        }
        return memo[i][j];
    }
};
// javascript-DP_table
var minDistance = function(word1, word2) {
    let m = word1.length;
    let n = word2.length;
    // 定义s1[0..i]和s2[0..j]的最小编辑距离是dp[i+1][j+1]
    let dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
    // base case
    for (let i = 1; i <= m; i++) {
        dp[i][0] = i;
    }
    for (let j = 1; j <= n; j++) {
        dp[0][j] = j;
    }
    // 自底向上求解
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (word1[i - 1] === word2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = Math.min(
                    dp[i - 1][j] + 1,
                    dp[i][j - 1] + 1,
                    dp[i - 1][j - 1] + 1
                );
            }
        }
    }
    // 储存着整个s1和s2的最小编辑距离
    return dp[m][n];
};

David-qiuwenhui avatar May 08 '22 06:05 David-qiuwenhui

在i=-1的时候,返回的为什么是j+1而不是j,因为在递归函数中i与j记录的字符串数组中的下标值,而数组的起始下标是从0开始的,j=-1时候同理

cillian-bao avatar May 15 '22 14:05 cillian-bao

DP Table 压缩空间 O(min(M, N))

空间复杂度是可以压缩成 O(min(M, N)) 的(M,N 是两个字符串的长度)。不难,但是可解释性大大降低,读者可以自己尝试优化一下。

应该是东哥说的 O(min(M,N)) 的空间复杂度了。

image
/**
 * @param {string} word1
 * @param {string} word2
 * @return {number}
 */
var minDistance = function (word1, word2) {
  // 交换,使得 word2 属于最短一方, 这样节约最省空间
  if (word1.length < word2.length) {
    let t = word2;
    word2 = word1;
    word1 = t;
  }

  // 初始化基准情况
  let dp = [];
  for (let i = 0; i <= word2.length; i++) {
    dp[i] = i;
  }

  // 状态转移
  for (let i = 1; i <= word1.length; i++) {
    // base case
    let dp_i = i - 1;
    dp[0] = i;
    for (let j = 1; j <= word2.length; j++) {
      let t = dp[j];
      if (word1[i - 1] === word2[j - 1]) {
        // skip
        dp[j] = dp_i;
      } else {
        dp[j] = 1 + Math.min(dp_i, dp[j - 1], dp[j]);
      }
      dp_i = t;
    }
  }

  return dp[word2.length];
};

dreamer2q avatar May 24 '22 14:05 dreamer2q

打卡,感谢楼主!

lihuagang03 avatar May 26 '22 13:05 lihuagang03

二维表这个解释厉害了

daydayup321 avatar May 29 '22 23:05 daydayup321

妙啊,东哥

LebronHarden1 avatar Jun 16 '22 03:06 LebronHarden1

交错字符串DP数组自下而上思路

class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        int len1 = s1.length();
        int len2 = s2.length();
        int len3 = s3.length();
        if(len1 + len2 != len3){
            return false;
        }
        //dp[i][j] 表示 s1[0..i-1] s2[0..j-1] 是否能平凑出 s3[0..i+j-1] 
        boolean[][] dp = new boolean[len1+1][len2+1];
        dp[0][0] = true;

        
        for(int i = 1; i<=len1;i++){
            //当前 s2 为空字符串的时候, 就需要看s3的字符是否与 s1 中的是否相同了 , 如果相同, 则可以平凑出来
            if(s1.charAt(i-1) == s3.charAt(i-1)){
                dp[i][0] = dp[i-1][0] && true ;
            }else{
                dp[i][0] = false;
            }
        }
        for(int j = 1 ; j<=len2;j++){
            if(s2.charAt(j-1) == s3.charAt(j-1)){
                dp[0][j] = dp[0][j-1];
            }else{
                dp[0][j] = false;
            }
        }

        for(int i = 1; i<=len1;i++){
            for(int j = 1 ; j<=len2;j++){
                //当前s3 位置的可以靠  s1 可以凑出 , 则当前位置的依靠前面的状态转换过来
                if(s1.charAt(i-1) == s3.charAt(i+j-1) ){
                    dp[i][j] = dp[i-1][j]; //当前位置i可以凑出来的, 那么就要看前面的i-1是否可以凑出来了
                }
                //同理
                if(s2.charAt(j-1) == s3.charAt(i+j-1)){
                    dp[i][j] = dp[i][j] || dp[i][j-1];
                }
            }
        }
        return dp[len1][len2];
    }
}

zhongweiLeex avatar Jul 20 '22 18:07 zhongweiLeex

通过学习 对动态规划进行降维打击 写出的压缩数组 自顶向下java写法

public int minDistance(String w1, String w2) {
    int r = w1.length()+1, c = w2.length()+1;
    
    int[] dp1 = new int[c];
    for(int i = 0; i < c; i++) {
        dp1[i] = i;
    }

    for( int i = 1; i < r; i ++) {
        int pre = i-1;
        dp1[0] = i;
        for ( int j = 1; j < c; j++) {
            int temp = dp1[j];
            if(w1.charAt(i-1) == (w2.charAt(j-1))) {
                dp1[j] = pre;
                // dp[i][j] = dp[i-1][j-1];
            } else {
                dp1[j] = Math.min(Math.min(dp1[j], pre), dp1[j-1]) +1;
                // dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i-1][j-1]), dp[i][j-1]) +1;
            }
            pre = temp;
        }
    }
    return dp1[c-1];
}

MzxlM avatar Aug 07 '22 17:08 MzxlM

【Java版空间压缩代码供参考】 能力有限所以没有实现@dreamer2q 同学的严格O(min(M,N))空间复杂度,不过马虎一下毕竟还是把复杂度降低了一个数量级(

// 空间压缩版代码
class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length(), n = word2.length();
        int[] dp = new int[n + 1];
        for (int j = 0; j <= n; j++) {
            dp[j] = j;
        }
        for (int i = 1; i <= m; i++) {
            int pre = i - 1;
            dp[0] = i;
            for (int j = 1; j <= n; j++) {
                int temp = dp[j];
                if (word1.charAt(i-1) == word2.charAt(j-1)) {
                    dp[j] = pre;
                } else {
                    dp[j] = min(pre, dp[j], dp[j - 1]) + 1;
                }
                pre = temp;
            }
        }
        return dp[n];
    }

    private int min(int a, int b, int c) {
        return Math.min(a, Math.min(b, c));
    }
}

YEthYuan avatar Aug 12 '22 09:08 YEthYuan

1、根据定义正着解释,两个字符串都可以操作,删除和替换 2、根据作者的倒着来但是 函数的话就倒着解释,dp数组的话就正着解释,只操作s1,增加、插入和删除

Velliy avatar Aug 15 '22 06:08 Velliy