这篇文章是记录关于LeetCode 动态规划系列专题的专栏, 全篇使用的是python版本的代码。
目录
1、正则表达式匹配
【难度:困难】
题目:给你一个字符串 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 = “cab”
输出:true
解释:因为 ‘*’ 表示零个或多个,这里 ‘c’ 为 0 个, 'a’被重复一次。因此可以匹配字符串 “aab”。
示例 5:
输入:s = “mississippi” p = “misisp*.”
输出:false
思路:该题可以采用典型的动态规划解法,动态规划的重点就是设定dp数组的定义以及找到状态转移方程。
假定我们用二维数组dp[i][j]来记录字符串s的前i个词与字符规律p的前j个词的匹配状态, 则首先明确的是,该数组的边界 dp[0][0] = True, 即两个空字符串肯定是匹配的。
接下来就是找状态转移方程,为了方便讨论,我们首先设定用 s(i):表示s的第i个词,p(j)表示p的第j个词,我们有以下讨论:
- p(j) 是 a-z的字母,但 s(i) != p(j), 这是最简单的情况,即二者不匹配 dp[i][j] = False
- p(j) 是 a-z的字母, 且s(i) == p(j), 此时我们只需要考察s的前i-1个词与p的前j-1个词是否匹配就可以确定了,即有状态转移方程: dp[i][j] = dp[i-1][j-1]
- p(j)是特殊字符 ‘.’, 根据题目描述 ‘.’是任意匹配的,所以情况与2.相同, 有状态转移方程: dp[i][j] = dp[i-1][j-1]
- p(j) 是特殊字符‘*’, 此时首先需要考察s(i)与p(j-1)是否匹配,根据情况再作讨论:
如果s(i) != p(j-1), 则说明该 ‘*’匹配了0次,此时只需要把该组合(星号和被星号修饰的字符)丢掉,考察 s(i) 与 p(j-2)是否匹配,
即有转移方程: dp[i][j] = dp[i][j-2]
如果 s(i) = p(j-1), 则要考虑的是星号组合到底重复了多少次, 是0次还是多次( 1次包括在多次里面):
a.如果星号组合重复了0次,例如:s=‘abcd’, p=‘abcc*d’, 此时需要考察 s(i)和p(j-2),即 dp[i][j] = dp[i][j-2];
b.如果星号组合重复出现了多次, 例如: s=‘abcccd’, p=‘abc*d’, 则我们可以丢掉当前的s(i),
去考察s(i-1)与p(j), 即 dp[i][j] = dp [i-1][j]。
所以此时两种情况有一种符合就可以判断s(i)与p(j)的状态,即 dp[i][j] = (dp[i-1][j] or dp[i][j-2])
【Python 实现】
"""10. 正则表达式匹配"""
def isMatch(self, s: str, p: str) -> bool:
"""
动态规划:dp[i][j] 表示s的前i个词与p的前j个词是否匹配
"""
dp = [[False for _ in range(len(p)+1)] for _ in range(len(s)+1)]
dp[0][0] = True # 初始状态
for i in range(len(s)+1): # i从0开始
for j in range(1, len(p)+1): # j从1开始
if i == 0: # s为空
if p[j-1] == '*':
dp[i][j] = dp[i][j-2]
else:
if s[i-1] == p[j-1] or p[j-1]=='.':
dp[i][j] = dp[i-1][j-1]
elif p[j-1] == '*':
if s[i-1] == p[j-2] or p[j-2] == '.': # s[i]==p[j-1]
# 讨论 s= abcd p= abc*d
# * 匹配一次或者多次, 则考察 dp[i-1][j] abccd, abc*d
# * 匹配0次 则考察 dp[i][j-2], 例: abcd, abcc*d
dp[i][j] = (dp[i-1][j] or dp[i][j-2])
else: # * 匹配0次
dp[i][j] = dp[i][j-2]
# print("S: {}, P: {}, {}".format(s[:i], p[:j], dp[i][j]))
return dp[len(s)][len(p)]
2、最长有效括号
【难度:困难】
题目:给定一个只包含 '('
和 ')'
的字符串,找出最长的包含有效括号的子串的长度。
示例1:
输入: "(()" 输出: 2 解释: 最长有效括号子串为"()"
示例2:
输入: ")()())
" 输出: 4 解释: 最长有效括号子串为"()()"
思路:笔者看到这题的第一反应是“这题也能困难难度吗?”,等我提交了第一个版本的代码之后才发现,卡在了第228/231个用例,超时了,果然是困难奥!-_-||,怎么回事呢,先说卡壳的解法一:
动态规划解法1(超时):我们常见的动态规划的dp数组是二维的,想当然的用dp[i][j]表示从第i个字符串到第j个字符串的状态,即是否是有效括号,只要找出字符串s的所有状态,那么我们就能计算出有效子串的长度。简单的分析之后我们发现:只有当考察的子串左边是"(",右边是")"时,才有可能符合有效括号子串的定义,此时括号子串可能有且只能有两种结构:1. (...)(...) 2.((....))。 第2种结构很容易可以找到其状态转移方程(dp[i][j]=dp[i+1][j-1]),第1种需要将左(....)右(....)两个结构拆开讨论,只要二者同时符合有效括号子串的定义,那么整个结构必然满足,按照此思路我们得到第一版代码:
python实现:
def longestValidParentheses(self, s: str) -> int:
"""
动态规划:dp[i][j]表示字符串s从i到j是否匹配, 在用例228/231处超时
"""
dp = [[False]*len(s) for _ in s]
maxL = 0
for i in range(len(s)-2, -1, -1):
temp = i # temp用来记录从左往右有效子括号的最右“)”
for j in range(i+1, len(s)):
if s[i] == "(" and s[j] == ")": # 只有当考察的子串左边是"(",右边是")"时,才有可能符合
if i+1 == j:
dp[i][j] = True
maxL = max(maxL, 2)
temp = j
elif (i+1) < (j-1): # i和j之间至少有两个字符
if (temp>i and dp[temp+1][j]) or dp[i+1][j-1]:
dp[i][j] = True
maxL = max(maxL, j-i+1)
temp = j
elif s[i] == ")":
break
return maxL
以上解法超时,我们只能再次重新考虑dp数组的设置,以寻求减少一层for循环,事实上,很多动态规划的二维dp数组都是可以优化为一维的。
动态规划解法2:我们重新定义动态规划的dp数组dp[i] : 表示以字符串的第i个字符s(i)结尾的有效括号的长度,注意:是以s[i]结尾的有效括号,什么意思呢?
举个例子:(())() 以最后一个")"结尾的有效括号是"()", 而不是"(())()", 注意到区别了吗?
这里的dp[i]一定不是我们通常所理解的以前i个括号的最大有效括号长度,而是以第i个括号作为结尾的有效括号的长度,那么以左括号"("结尾 的dp[i]必定是0。
基于以上思路,我们可以轻松得出动态规划的转移方程,要考察dp[i], 首先必须考察是否s[i]==")", 如果 s[i]=="(", 那么dp[i]必然为0,否则咱们需要借助dp[i-1]找到与s[i]对应的括号,其位置为 j=i-dp[i-1]-1
如果s[j]=="(", 那么说明dp[i]的有效长度至少是dp[i-1]+2, 同时我们只需考察dp[j-1]的长度就可以得出真正的dp[i] = dp[i-1] + 2 + (dp[j-1] if j>0 else 0), 是不是简单了不少呢? 可见找到合适的dp数组对于解决动态规划问题是多么的关键!
第二版本的动态规划解法如下:
python实现
def longestValidParentheses2(self, s: str) -> int:
"""
动态规划:dp[i]表示以字符s[i]结尾的有效括号的长度,注意:是以s[i]结尾的有效括号,什么意思呢,
举个例子:(())() 以最后一个")"结尾的有效括号是"()", 而不是"(())()", 注意到区别了吗?
这里的dp[i]一定不是我们通常所理解的以前i个括号的最大有效括号长度,而是以第i个括号作为结尾的有效括号的长度,
那么以左括号"("结尾 的dp[i]必定是0
"""
dp = [0 for _ in s]
if (not s) or len(s) < 2:
return 0
maxL = 0
for i in range(1, len(s)): # 从第2个括号开始遍历
if s[i] == ")" :
j = i - dp[i-1] -1 # 找与右括号对应的匹配的左括号的位置
if j>=0 and s[j] == "(":
dp[i] = (i-j+1) + (dp[j-1] if j>0 else 0)
maxL = max(maxL, dp[i])
return maxL
(未完待续...)