区间动态规划例题:正则表达式匹配问题(支持 '.' 和 '*' 以及 '+' 和 '?' 的量词)
字数 2940 2025-12-05 21:41:54

区间动态规划例题:正则表达式匹配问题(支持 '.' 和 '*' 以及 '+' 和 '?' 的量词)

题目描述
给定一个字符串 s 和一个模式串 p,实现支持以下规则的正则表达式匹配:

  • '.' 匹配任意单个字符。
  • '*' 匹配零个或多个前面的元素。
  • '+' 匹配一个或多个前面的元素。
  • '?' 匹配零个或一个前面的元素。

匹配应该覆盖整个字符串 s,而不是部分字符串。
你需要判断 s 是否与 p 完全匹配。

例如:
输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配整个字符串 "aa"。

输入:s = "aa", p = "a*"
输出:true
解释:'*' 表示前面的元素 'a' 重复零次或多次,这里重复两次 "aa"。

输入:s = "ab", p = "."
输出:true
解释:".
" 表示零个或多个任意字符,可以匹配 "ab"。

输入:s = "aab", p = "cab"
输出:true
解释:c 可以重复零次,a 重复两次,b 匹配 b。

输入:s = "ab", p = "a?b"
输出:true
解释:'?' 表示前面的 a 出现零次或一次,这里出现一次,匹配 "ab"。

输入:s = "aaa", p = "a+"
输出:true
解释:'+' 表示前面的 a 至少出现一次,这里出现三次。


解题思路

本题是经典正则表达式匹配问题的扩展,加入了 '+' 和 '?' 量词。我们可以使用区间动态规划来解决,定义二维 DP 数组,然后根据模式串中的字符和量词,分情况讨论状态转移。


步骤详解

步骤1:定义DP数组

  • 定义 dp[i][j] 表示:s 的前 i 个字符(s[0..i-1])与 p 的前 j 个字符(p[0..j-1])是否匹配。
  • 初始时,dp[0][0] = true,表示空字符串匹配空模式。

步骤2:初始化空模式匹配非空字符串的情况

  • 对于 i > 0j = 0dp[i][0] = false,因为非空字符串无法匹配空模式。

步骤3:初始化空字符串匹配模式的情况

  • 对于 j > 0,即模式非空,需要考虑量词 '*'、'?' 可能表示零个字符的情况,从而让模式匹配空字符串。
  • 规则:如果 p[j-1] 是 '' 或 '?',那么 dp[0][j] 的值取决于 dp[0][j-2](跳过前面的字符和量词),因为 '' 或 '?' 可以匹配零个前面的元素。
  • 注意:如果 p[j-1] 是 '+',则不能匹配零个,所以不能匹配空字符串,保持 false。

步骤4:状态转移(填表)
遍历 i 从 0 到 n(n 为 s 长度),j 从 1 到 m(m 为 p 长度),对每个 dp[i][j] 分情况讨论:

  1. 如果 p[j-1] 是普通字符(不是 '.' 和量词)

    • 则需要 s[i-1] 等于 p[j-1],且前面的部分匹配,即:
      dp[i][j] = (s[i-1] == p[j-1]) && dp[i-1][j-1]
  2. 如果 p[j-1] 是 '.'

    • '.' 可以匹配任意单个字符,所以只要前面部分匹配即可:
      dp[i][j] = dp[i-1][j-1]
  3. 如果 p[j-1] 是量词('*'、'?'、'+')

    • 量词必须和它前面的字符一起处理,所以我们需要看 p[j-2] 是什么字符(这里 j ≥ 2)。
    • prev = p[j-2](前面的字符)。
    • 然后根据量词类型处理:

    a) '*' 量词(零个或多个 prev)

    • 两种情况取或:
      • 匹配零个 prev:跳过 "prev*",即 dp[i][j] = dp[i][j-2]
      • 匹配一个或多个 prev:需要当前 s[i-1] 能匹配 prev(即 s[i-1]==prev 或 prev=='.'),并且 s 的前 i-1 个字符能匹配当前模式(即 dp[i-1][j] 为真)。
    • 综合:dp[i][j] = dp[i][j-2] || ( (s[i-1]==prev || prev=='.') && dp[i-1][j] )

    b) '?' 量词(零个或一个 prev)

    • 两种情况取或:
      • 匹配零个 prev:跳过 "prev?",即 dp[i][j] = dp[i][j-2]
      • 匹配一个 prev:需要当前 s[i-1] 能匹配 prev,且前面部分匹配,即 (s[i-1]==prev || prev=='.') && dp[i-1][j-2]
    • 注意:匹配一个时,模式中 "prev?" 消耗了模式的两个字符,所以是 j-2
    • 综合:dp[i][j] = dp[i][j-2] || ( (s[i-1]==prev || prev=='.') && dp[i-1][j-2] )

    c) '+' 量词(一个或多个 prev)

    • 两种情况:
      • 匹配一个 prev:需要当前 s[i-1] 匹配 prev,并且前面部分匹配模式中 "prev+" 之前的部分,即 (s[i-1]==prev || prev=='.') && dp[i-1][j-2]
      • 匹配多个 prev:需要当前 s[i-1] 匹配 prev,并且 s 的前 i-1 个字符能匹配当前模式(即 dp[i-1][j] 为真),因为 '+' 表示至少一个,消耗一个后模式不变。
    • 注意:这里不能匹配零个,所以没有 dp[i][j-2]
    • 综合:dp[i][j] = ( (s[i-1]==prev || prev=='.') && (dp[i-1][j-2] || dp[i-1][j]) )

步骤5:最终结果

  • 结果存储在 dp[n][m] 中,即整个字符串 s 和整个模式 p 是否匹配。

示例推演

以 s = "aab", p = "cab" 为例(只包含 '*' 量词,用于简单演示):

  • n=3, m=5。
  • 初始化 dp[0][0]=true。
  • 初始化 dp[0][j]:p="cab" 中 '' 可以匹配零个,所以 dp[0][2]=true(跳过 c),dp[0][4]=true(跳过 a*),最终 dp[0][5]=true。
  • 填表过程(略过详细单元格计算),最终 dp[3][5]=true,匹配成功。

复杂度分析

  • 时间复杂度:O(n × m),其中 n 为 s 长度,m 为 p 长度。
  • 空间复杂度:O(n × m),可优化到 O(m) 但实现稍复杂。

关键点

  1. 处理量词时要考虑前面字符 prev,并且分情况讨论零个、一个、多个的匹配。
  2. 初始化空字符串匹配模式时,只有 '*' 和 '?' 能让模式匹配空串,'+' 不行。
  3. 状态转移时,注意下标不要越界(例如 j≥2 时才处理量词)。

通过以上步骤,我们可以判断字符串 s 是否与带有 '.'、'*'、'+'、'?' 的正则表达式模式 p 完全匹配。

区间动态规划例题:正则表达式匹配问题(支持 '.' 和 '* ' 以及 '+' 和 '?' 的量词) 题目描述 给定一个字符串 s 和一个模式串 p,实现支持以下规则的正则表达式匹配: '.' 匹配任意单个字符。 '* ' 匹配零个或多个前面的元素。 '+' 匹配一个或多个前面的元素。 '?' 匹配零个或一个前面的元素。 匹配应该覆盖整个字符串 s,而不是部分字符串。 你需要判断 s 是否与 p 完全匹配。 例如: 输入:s = "aa", p = "a" 输出:false 解释:"a" 无法匹配整个字符串 "aa"。 输入:s = "aa", p = "a* " 输出:true 解释:'* ' 表示前面的元素 'a' 重复零次或多次,这里重复两次 "aa"。 输入:s = "ab", p = ". " 输出:true 解释:". " 表示零个或多个任意字符,可以匹配 "ab"。 输入:s = "aab", p = "c a b" 输出:true 解释:c 可以重复零次,a 重复两次,b 匹配 b。 输入:s = "ab", p = "a?b" 输出:true 解释:'?' 表示前面的 a 出现零次或一次,这里出现一次,匹配 "ab"。 输入:s = "aaa", p = "a+" 输出:true 解释:'+' 表示前面的 a 至少出现一次,这里出现三次。 解题思路 本题是经典正则表达式匹配问题的扩展,加入了 '+' 和 '?' 量词。我们可以使用 区间动态规划 来解决,定义二维 DP 数组,然后根据模式串中的字符和量词,分情况讨论状态转移。 步骤详解 步骤1:定义DP数组 定义 dp[i][j] 表示:s 的前 i 个字符(s[ 0..i-1])与 p 的前 j 个字符(p[ 0..j-1 ])是否匹配。 初始时, dp[0][0] = true ,表示空字符串匹配空模式。 步骤2:初始化空模式匹配非空字符串的情况 对于 i > 0 且 j = 0 , dp[i][0] = false ,因为非空字符串无法匹配空模式。 步骤3:初始化空字符串匹配模式的情况 对于 j > 0 ,即模式非空,需要考虑量词 '* '、'?' 可能表示零个字符的情况,从而让模式匹配空字符串。 规则:如果 p[j-1] 是 ' ' 或 '?',那么 dp[0][j] 的值取决于 dp[0][j-2] (跳过前面的字符和量词),因为 ' ' 或 '?' 可以匹配零个前面的元素。 注意:如果 p[j-1] 是 '+',则不能匹配零个,所以不能匹配空字符串,保持 false。 步骤4:状态转移(填表) 遍历 i 从 0 到 n(n 为 s 长度),j 从 1 到 m(m 为 p 长度),对每个 dp[i][j] 分情况讨论: 如果 p[ j-1] 是普通字符(不是 '.' 和量词) : 则需要 s[ i-1] 等于 p[ j-1 ],且前面的部分匹配,即: dp[i][j] = (s[i-1] == p[j-1]) && dp[i-1][j-1] 。 如果 p[ j-1] 是 '.' : '.' 可以匹配任意单个字符,所以只要前面部分匹配即可: dp[i][j] = dp[i-1][j-1] 。 如果 p[ j-1] 是量词('* '、'?'、'+') : 量词必须和它前面的字符一起处理,所以我们需要看 p[ j-2 ] 是什么字符(这里 j ≥ 2)。 设 prev = p[j-2] (前面的字符)。 然后根据量词类型处理: a) '* ' 量词(零个或多个 prev) : 两种情况取或: 匹配零个 prev:跳过 "prev* ",即 dp[i][j] = dp[i][j-2] 。 匹配一个或多个 prev:需要当前 s[ i-1] 能匹配 prev(即 s[ i-1]==prev 或 prev=='.'),并且 s 的前 i-1 个字符能匹配当前模式(即 dp[i-1][j] 为真)。 综合: dp[i][j] = dp[i][j-2] || ( (s[i-1]==prev || prev=='.') && dp[i-1][j] ) 。 b) '?' 量词(零个或一个 prev) : 两种情况取或: 匹配零个 prev:跳过 "prev?",即 dp[i][j] = dp[i][j-2] 。 匹配一个 prev:需要当前 s[ i-1] 能匹配 prev,且前面部分匹配,即 (s[i-1]==prev || prev=='.') && dp[i-1][j-2] 。 注意:匹配一个时,模式中 "prev?" 消耗了模式的两个字符,所以是 j-2 。 综合: dp[i][j] = dp[i][j-2] || ( (s[i-1]==prev || prev=='.') && dp[i-1][j-2] ) 。 c) '+' 量词(一个或多个 prev) : 两种情况: 匹配一个 prev:需要当前 s[ i-1] 匹配 prev,并且前面部分匹配模式中 "prev+" 之前的部分,即 (s[i-1]==prev || prev=='.') && dp[i-1][j-2] 。 匹配多个 prev:需要当前 s[ i-1] 匹配 prev,并且 s 的前 i-1 个字符能匹配当前模式(即 dp[i-1][j] 为真),因为 '+' 表示至少一个,消耗一个后模式不变。 注意:这里不能匹配零个,所以没有 dp[i][j-2] 。 综合: dp[i][j] = ( (s[i-1]==prev || prev=='.') && (dp[i-1][j-2] || dp[i-1][j]) ) 。 步骤5:最终结果 结果存储在 dp[n][m] 中,即整个字符串 s 和整个模式 p 是否匹配。 示例推演 以 s = "aab", p = "c a b" 为例(只包含 '* ' 量词,用于简单演示): n=3, m=5。 初始化 dp[ 0][ 0 ]=true。 初始化 dp[ 0][ j]:p="c a b" 中 ' ' 可以匹配零个,所以 dp[ 0][ 2]=true(跳过 c ),dp[ 0][ 4]=true(跳过 a* ),最终 dp[ 0][ 5 ]=true。 填表过程(略过详细单元格计算),最终 dp[ 3][ 5 ]=true,匹配成功。 复杂度分析 时间复杂度:O(n × m),其中 n 为 s 长度,m 为 p 长度。 空间复杂度:O(n × m),可优化到 O(m) 但实现稍复杂。 关键点 处理量词时要考虑前面字符 prev,并且分情况讨论零个、一个、多个的匹配。 初始化空字符串匹配模式时,只有 '* ' 和 '?' 能让模式匹配空串,'+' 不行。 状态转移时,注意下标不要越界(例如 j≥2 时才处理量词)。 通过以上步骤,我们可以判断字符串 s 是否与带有 '.'、'* '、'+'、'?' 的正则表达式模式 p 完全匹配。