每天下班了都要想一会这个题,今天终于自己能给出一个自己比较满意的答案,最好是能通过今天这题给出一个简单的方法论来.
10. 正则表达式匹配
题目
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
示例
示例 1:
输入:s = "aa" p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:s = "aa" p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:
输入:s = "ab" p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4:
输入:s = "aab" p = "c*a*b"
输出:true
解释:因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
示例 5:
输入:s = "mississippi" p = "mis*is*p*."
输出:false
提示:
0 <= s.length <= 20
0 <= p.length <= 30
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
保证每次出现字符 * 时,前面都匹配到有效的字符
题解
先看维基对于dp的一些描述.
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。
适用于dp的问题的特点
- 重叠子问题
重叠子问题就是算过的值可能会被复用到.比如计算已知了1+1 =2 ,计算1+1+1的时候就可以复用之前的结果,直接把问题转化为计算2+1 - 最优子结构
最优子结构的意思是局部最优解能决定全局最优解.
或者说可以从子问题的最优结果推出更大规模问题的最优结果.
来看看我们这个问题,我们要明确知道:
- 是否存在子问题决定全局的情况,即是否存在最优子问题
- 历史计算结果能否被复用,即是否存在重叠子问题.
为了更好地化解这个问题,我们不管什么字符不字符的,我们使用形式化的方式对这个问题进行描述,即dp[i][j]
来表达s的子串和p的子串的匹配关系.
更多的,我们需要考虑下,空串如何处理.很简单,空串可以由空串匹配,或者由@*
来匹配,@
为任意的合法字符.其他的情况是不合法的.所以我们可以很容易的得到下面的表:
OK,我们现在已经用形式化的写法描述了我们状态,状态True意味着子串可以匹配,否则就是不能匹配;同时我么还有了一个非常坚实的基础,dp[0][0]是True.现在缺少的就是判断是否存在可以由子问题推导出更大规模问题的方法了,如果存在,我们的用动态规划解题就是合理的.
我们可以做一个简单的子问题类型划分:
s[i] == p[j] || s[i] != p[j]
如果这两种情况我们都可以处理,那就是最优子结构没问题了.
第一种情况:s[i] == p[j]
这意味着当前两个字符是完全可以匹配的,如果之前的状态匹配,那么毫无疑问,那么当前的状态也肯定是匹配,否则就是不匹配.
即存在状态转移方程:
dp[i][j] = dp[i-1][j-1]
第二种情况s[i] != p[j]
这个情况相对复杂,我们可以再分成三类来考虑:
p[i] == '.' || p[i] == others || p[i] == '*'
(1) p[i]
是通配符p[i] == '.'
通配符可以看做和第一情况是一样的,因为它也存在状态转移方程:
dp[i][j] = dp[i-1][j-1]
(2) p[i]
不是特殊符号p[i] == others
这就没得说了,肯定是不匹配了,要看看是不是当前p[j] ==’*’,如果是的话,可能会存在@*
是0字符的情况
dp[i][j] == dp[i][j-2]
(3) p[i]
是 ‘*’p[i] == '*'
我们可以仔细想一个问题,'@*'
能代表什么?
@*
可以代表三种情况
<1> 空串
此时重复0次–>@*
在s中占位为0,相当于空串
a with ab*
<2> 单字符
此时重复1次–>@*
在s中占位为1,相当于单个字符
ab with ab*
<3> 重复字符串
重复n次–>@*
在s中占n位
abbbbbbbbb with ab*
只要dp[i][j]
的上述三种情况存在一种,那么他就能匹配
所以存在状态转移方程
dp[i][j] = dp[i-1][j] or dp[i][j-1] or dp[i][j-2]
到这一步,我们发现所有的状态都能用前序状态推导出来或者根据当前信息直接获取,因此我们完全可以用动态规划来解决这个问题.
贴一下代码:
def isMatch(s: str, p: str) -> bool:
len_s = len(s)
len_p = len(p)
dp = [[False] * (len_p + 1) for _ in range(len_s + 1)]
dp[0][0] = True
for j in range(1, len_p + 1):
if p[j - 1] == '*':
dp[0][j] = dp[0][j - 2]
for i in range(1, len_s + 1):
for j in range(1, len_p + 1):
#p[0]都是不用判断的
if p[j - 1] in {s[i - 1], '.'}:
dp[i][j] = dp[i - 1][j - 1]
elif p[j - 1] == '*':
if p[j - 2] in {s[i - 1], '.'}:
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - 2] or dp[i][j - 2]
else:
dp[i][j] = dp[i][j - 2]
return dp[len_s][len_p]
最后我总结一下:
遇到这类问题首先进行分析,不一定就是可以用dp的,判断的方式就是寻找最优子结构和当前状态的对应关系.具体可以分为三步:
- 用形式化的方式描述问题(本题就是dp[i][j]代表能否匹配),这意味后续代码之和状态有关,和问题无关;
- 找到初始状态,这是求解问题的根基;
- 根据问题寻找状态转移的方式
如果这些都没问题,那剩下的就是最简单的写代码了.