10.5.1 (python) 动态规划字符串类LeetCode题目 —— Edit Distance & Regular Expression Matching

下面学习几道字符串的题目,难度一般较大,但是也有共性,有套路,必须掌握几道经典的题目。

 

72. Edit Distance

Given two words word1 and word2, find the minimum number of operations required to convert word1 to word2.

You have the following 3 operations permitted on a word:

  1. Insert a character
  2. Delete a character
  3. Replace a character

题目解析:

先摆上这道编辑距离的大餐,我们细细解析。既然这是动态规划,我们来套一套DP的解法框架,后面类似的字符串题目都基本迎刃而解。

首先,我们定义问题的最优子结构,以及dp数组的含义。将Word1转换成Word2,我们定义二维数组dp[i][j],其含义为将长度为i的字符串转为长度为j的字符串的操作次数。这个套路适用于很多字符串题目。

然后,最重要的是,状态转移关系。不难想到,dp[i][j]一定和dp[i-1][j],dp[i][j-1]和dp[i-1][j-1]有关。

如果word[i]与word[j]相等,此时不用任何操作,dp[i][j]=dp[i-1][j-1];

如果不相等,那么我们可以通过三种操作进行调整:1,替换,Word1[i]换成Word2[j],那么dp[i][j]=dp[i-1][j-1]+1;2,插入,在Word1[i]末尾插入一个字符Word2[j],那么dp[i][j]=dp[i][j-1]+1;3,删除,把Word1[i]字符删除, dp[i][j]=dp[i-1][j]+1. 于是 dp[i][j]=min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j]) + 1。关系式就是这样,还是很好记的,但是要理解解题逻辑才能解决更多题目。

最后是边界问题。当i为0时,只能通过插入字符;当j为0时,只能通过删除操作。

详细的看代码细细理解。

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        if not word1:
            return len(word2)
        if not word2:
            return len(word1)
        
        dp = [[0] * (len(word2)+1) for _ in range(len(word1)+1)]

        for i in range(len(word2)):
            dp[0][i+1] = i+1
        for i in range(len(word1)):
            w1 = word1[i]
            dp[i+1][0] =  i+1
            for j in range(len(word2)):

                if w1 == word2[j]:
                    dp[i+1][j+1] = dp[i][j]
                else:
                    dp[i+1][j+1] = min(dp[i][j+1], dp[i+1][j], dp[i][j]) + 1
        return dp[-1][-1]        

 

10. Regular Expression Matching

Given an input string (s) and a pattern (p), implement regular expression matching with support for '.' and '*'.

'.' Matches any single character.
'*' Matches zero or more of the preceding element.

The matching should cover the entire input string (not partial).

Note:

  • s could be empty and contains only lowercase letters a-z.
  • p could be empty and contains only lowercase letters a-z, and characters like . or *.

题目解析:

我们看这道正则式匹配的题目。仍然是两个字符串之间的关系。这里我们不详细解释,全在代码及注释中。

1. 关于dp[i][j]数组的定义,与上题类似;

2. 状态转移关系:也要分多种情况,字符一样,为通配符等,每种情况有状态转移关系,建议自己举例子理解一下;

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        dp = [[0] * (len(p)+1) for _ in range(len(s)+1)]  # dp[i][j] 表示 s 的前 i 个是否能被 p 的前 j 个匹配
        dp[0][0] = True
        for i in range(len(p)):
            if p[i] == "*" or i < len(p)-1 and p[i+1] == "*" :
                dp[0][i+1] = dp[0][i]

        for i in range(len(s)):
            
            for j in range(len(p)):
                # 字母一样
                if s[i] == p[j]:
                    dp[i+1][j+1] = dp[i][j]
                    continue
                # 字母不同但是下一个是通配符*可以匹配0个
                if p[j] != "." and j+1 < len(p) and p[j+1] == "*":
                    dp[i+1][j+1] = dp[i][j]
                    continue
                # 通配符 .
                if p[j] == ".":
                    dp[i+1][j+1] = dp[i][j]
                    continue
                # 通配符 *
                if p[j] == "*":
                    # 考虑p前一个字符 可能是a-z 或者 .
                    if s[i] == p[j-1] or p[j-1] == ".":
                    # dp[i][j] = dp[i-1][j] // 多个字符匹配的情况	
                    # or dp[i][j] = dp[i][j-1] // 单个字符匹配的情况
                    # or dp[i][j] = dp[i][j-2] // 没有匹配的情况
                        dp[i+1][j+1] = dp[i][j+1] or dp[i+1][j] or dp[i+1][j-1]
                    else:
                        # 匹配不上,认为匹配0次
                        dp[i+1][j+1] = dp[i+1][j-1]

        return True if dp[-1][-1] == 1 else False

此题还有回溯的方法 。

如果没有星号(正则表达式中的 * ),问题会很简单——我们只需要从左到右检查匹配串 s 是否能匹配模式串 p 的每一个字符。

当模式串中有星号时,我们需要检查匹配串 s 中的不同后缀,以判断它们是否能匹配模式串剩余的部分。一个直观的解法就是用回溯的方法来体现这种关系。

如果模式串中有星号,它会出现在第二个位置,即 \text{pattern[1]}pattern[1] 。这种情况下,我们可以直接忽略模式串中这一部分,或者删除匹配串的第一个字符,前提是它能够匹配模式串当前位置字符,即 \text{pattern[0]}pattern[0] 。如果两种操作中有任何一种使得剩下的字符串能匹配,那么初始时,匹配串和模式串就可以被匹配。

但是这一方法跑起来很慢的,毕竟重复计算较多。

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        def match_help(ix1, ix2):
            if ix2 == len(p):
                return ix1 == len(s)
            first_match = ix1 != len(s) and (s[ix1] == p[ix2] or p[ix2] == ".")
            if ix2 < len(p) - 1 and p[ix2+1] == "*":
                return match_help(ix1, ix2+2) or first_match and match_help(ix1+1, ix2)
            else:
                return first_match and match_help(ix1+1, ix2+1)

        return match_help(0, 0)    

这两道题非常经典,务必掌握字符串类问题动态规划的套路。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值