下面学习几道字符串的题目,难度一般较大,但是也有共性,有套路,必须掌握几道经典的题目。
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:
- Insert a character
- Delete a character
- 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 lettersa-z
.p
could be empty and contains only lowercase lettersa-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)
这两道题非常经典,务必掌握字符串类问题动态规划的套路。