Leetcode 题解之动态规划系列篇

这篇文章是记录关于LeetCode 动态规划系列专题的专栏, 全篇使用的是python版本的代码。

目录

1、正则表达式匹配

2、最长有效括号


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个词,我们有以下讨论:

  1. p(j) 是 a-z的字母,但 s(i) != p(j), 这是最简单的情况,即二者不匹配 dp[i][j] = False
     
  2. p(j) 是 a-z的字母, 且s(i) == p(j), 此时我们只需要考察s的前i-1个词与p的前j-1个词是否匹配就可以确定了,即有状态转移方程: dp[i][j] = dp[i-1][j-1]
     
  3. p(j)是特殊字符 ‘.’, 根据题目描述 ‘.’是任意匹配的,所以情况与2.相同, 有状态转移方程: dp[i][j] = dp[i-1][j-1]
     
  4. 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

(未完待续...)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值