[LeetCode dynamic planning special] editing distance (72)

1. Title

Here are two words word1 and word2. Please calculate the minimum operands used to convert word1 to word2. You can perform the following three operations on a word:

  • Insert a character
  • Delete a character
  • Replace one character

1.1 example

  • Example 1 1 1:
  • Input: word1 = "horse", word2 = "ros"
  • Output: 3 3 3
  • Explanation:
    • Horse - > horse (replace 'h' with 'r')
    • Rose - > Rose (delete 'r')
    • Rose - > ROS (delete 'e')
  • Example 2 2 2:
  • Input: word1 = "intention", word2 = "execution"
  • Output: 5 5 5
  • Explanation:
    • Intent - > intent (delete't ')
    • Inention - > enention (replace 'i' with 'e')
    • Generation - > exception (replace 'n' with 'x')
    • Exception - > exception (replace 'n' with 'c')
    • Execution - > execution (insert 'u')

1.2 description

1.3 tips

  • 0 <= word1.length, word2.length <= 500 ;
  • word1 and word2 are composed of lowercase English letters.

1.4 advanced

Can you further output the specific operation of each of the minimum number of operations?

2. Solution I (simple recursion)

2.1 analysis

At first glance, there seems to be no way to start the problem, because it seems that there is no other way but to try all possible sequences of operations. Usually at this time, recursion is a possible way to traverse all possible situations.

It should be noted that for the implementation of recursion, the first thing to consider is the exit of recursion, otherwise infinite recursion will lead to stack overflow. For strings, it is natural to consider using the character index to represent the States before and after each recursion, and then consider which states represent the exit of recursion.

In this regard, since the title is given two strings, two indexes are required here. Accordingly, the following defines the exit of recursion and various situations of recursive call respectively:

2.1.1 recursive exit

Obviously, when word1 or word2 is an empty string '', the minimum number of operations can be obtained directly, that is, the length of the non empty string in both. For example, when word1 = "horse" and word2 = "", word1 needs to be modified at least 5 5 word2 can only be obtained after 5 deletion operations.

2.1.2 recursive call

When word1 and word2 are not empty, they can be further divided into the following two cases, specifically:

  1. When word1[i] == word2[j], as shown in the figure below, there is no need to do any operation in insertion, deletion or replacement, that is, the problem is equivalent to considering how to convert word1[1:] into word2[1:] through the minimum number of operations;

  2. When word1 [i]= Word2 [J], at this time, it can be further divided into two types according to the next operation of character insertion, deletion or replacement 3 3 3 cases:

  • Insert: at this time, the solution of the problem is equivalent to considering using 1 1 How to convert word1 to word2[1:] through the minimum number of operations after one insertion operation;

  • Delete: at this time, the solution of the problem is equivalent to considering using 1 1 How to convert word1[1:] to word2 through the minimum number of operations after one deletion;

  • Replacement: at this time, the solution of the problem is equivalent to considering the use of 1 1 How to convert word1[1:] to word2[1:] through the minimum number of operations after a replacement operation.

2.2 answers

According to the above analysis, we can easily get the following code implementation based on naive recursion:

class Solution:
    def min_distance(self, word1: str, word2: str) -> int:
        if len(word1) == 0:
            return len(word2)
        if len(word2) == 0:
            return len(word1)
        if word1[0] == word2[0]:
            return self.min_distance(word1[1:], word2[1:])
        else:
            return min(1 + self.min_distance(word1, word2[1:]),
                       1 + self.min_distance(word1[1:], word2),
                       1 + self.min_distance(word1[1:], word2[1:]))


def main():
    word1 = "intention"
    word2 = "execution"
    sln = Solution()
    print(sln.min_distance(word1, word2))  # 5


if __name__ == '__main__':
    main()

2.3 complexity

Although the above simple recursive solution is very concise and elegant, its time complexity is as high as O ( 3 min(len(word1), len(word2)) ) O(3^{\text{min(len(word1), len(word2))}}) O(3min(len(word1),   Len (word2)), if you submit directly, you will receive a prompt from the platform that the time limit is exceeded.

3. Solution 2 (top-down cache recursion)

3.1 analysis

The reason why the above simple recursive solution is inefficient is that it repeatedly calculates many sub problems. For example, when word1 = "horse" and word2 = "hello" and the recursive call depth is 3 3 At 3, the sub problem is solved as follows:

md("horse", "hello")
	md("orse", "ello")
		md("orse", "llo")
			md("orse", "lo")
			md("rse", "llo") <- 
			md("rse", "lo")
		md("rse", "ello")
			md("rse", "llo") <-
			md("se", "ello")
			md("se", "llo") <<-
		md("rse", "llo")
			md("rse", "llo") <-
			md("se", "llo") <<-
			md("se", "lo")

The first way to solve the repeated subproblem is to use additional containers to store it. In this way, after the subproblem results are calculated for the first time, they can be directly obtained from the stored results when the subproblem needs to be solved again next time without repeated calculation:

3.2 answers

from typing import Dict


class Solution:
    def memoized_min_distance(self, word1: str, word2: str, i: int, j: int, memo: Dict) -> int:
        if i == len(word1):
            return len(word2) - j
        if j == len(word2):
            return len(word1) - i
        if (i, j) not in memo.keys():
            if word1[i] == word2[j]:
                num = self.memoized_min_distance(word1, word2, i + 1, j + 1, memo)
            else:
                num = min(1 + self.memoized_min_distance(word1, word2, i, j + 1, memo),
                          1 + self.memoized_min_distance(word1, word2, i + 1, j, memo),
                          1 + self.memoized_min_distance(word1, word2, i + 1, j + 1, memo))
            memo[(i, j)] = num
        return memo[(i, j)]


def main():
    word1 = "dinitrophenylhydrazine"
    word2 = "acetylphenylhydrazine"
    sln = Solution()
    memo = dict()
    print(sln.memoized_min_distance(word1, word2, 0, 0, memo))  # 6


if __name__ == '__main__':
    main()

  • Execution time: 104 ms, beating 94.65% of users in all Python 3 submissions;
  • Memory consumption: 17.3 MB, beating 93.47% of users in all Python 3 submissions.

3.3 complexity

  • Time complexity: O ( m n ) O(mn) O(mn) ;
  • Space complexity: O ( m n ) O(mn) O(mn) .

4. Solution 3 (bottom-up dynamic programming iteration)

4.1 analysis

Although the above top-down cache recursive solution has greatly improved the efficiency of the solution, it not only consumes a lot of resources each time, but also the recursion is limited by the maximum depth. Therefore, the iterative solution is further considered.

Here, the iterative solution of dynamic programming also uses an auxiliary storage container. Here, a two-dimensional list dp is used. Each element dp[i][j] in the list represents the minimum number of operations required to convert the substring composed of the first I strings of word1 into the substring composed of the first j characters of word2.

4.2 answers

from typing import Dict


class Solution:
    def iterative_min_distance(self, word1: str, word2: str) -> int:
        m, n = len(word1), len(word2)
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        for i in range(m + 1):
            dp[i][0] = i
        for j in range(n + 1):
            dp[0][j] = j
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if word1[i - 1] == word2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1]
                else:
                    dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
        return dp[-1][-1]


def main():
    word1 = "dinitrophenylhydrazine"
    word2 = "acetylphenylhydrazine"
    sln = Solution()
    print(sln.iterative_min_distance(word1, word2))  # 6


if __name__ == '__main__':
    main()

  • Execution time: 120 ms, beating 90.81% of users in all Python 3 submissions;
  • Memory consumption: 18.3 MB, beating 88.78% of users in all Python 3 submissions

4.3 complexity

  • Time complexity: O ( m n ) O(mn) O(mn) ;
  • Space complexity: O ( m n ) O(mn) O(mn) .

Tags: leetcode Dynamic Programming

Posted on Sat, 25 Sep 2021 04:42:57 -0400 by MattDunbar