使用动态规划实现正则表达式匹配

严正声明:本文系作者davidhopper原创,未经许可,不得转载。

下述问题来源于正则表达式匹配

一、问题描述

给定一个字符串 s 和一个字符串模式p,请你来实现一个支持.*的正则表达式匹配。

  1. .匹配任意单个字符
  2. *匹配零个或多个前面的那一个元素
    所谓匹配,是要涵盖整个字符串s,而不是部分字符串。

说明:

  1. s可能为空,且只包含从a-z的小写字母。
  2. p可能为空,且只包含从a-z的小写字母,以及字符.*

示例 1:

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

示例 2:

输入:
s = "aa"
p = "a*"
输出: 
true
解释: 
因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。
因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3:

输入:
s = "ab"
p = ".*"
输出: 
true
解释: 
".*" 表示可匹配零个或多个('*')任意字符('.')。
首先,".*"匹配'a',接下来".*"匹配' b'。

示例 4:

输入:
s = "aab"
p = "c*a*b"
输出: 
true
解释: 
因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。

示例 5:

输入:
s = "mississippi"
p = "mis*is*p*."
输出: 
false
解释: 
因为 '*' 表示零个或多个,'s*p'不能匹配'ssip' 。

二、动态规划思想

动态规划与分治法(the divide-and-conquer method)有些类似,也是将问题分解为多个子问题,并且基于子问题的结果获得最终解。二者的区别是,分治法将初始问题划分为多个不关联(disjoint)的子问题(subproblem)(即子问题互不依赖),递归地解决子问题,然后将子问题的解合并得到初始问题的解。与之相反,动态规划法分解得到的子问题是相互重叠的(overlap),即子问题依赖于子子问题(subsubproblem),子子问题又进一步依赖于下一级的子子问题,这样不断循环直至抵达不需分解的初始条件。在求解过程中,为了避免重复计算子子问题从而提高算法效率,需要将一系列子子问题的解保存到一张表中(table,C++编程一般使用std::arraystd::vectorstd::list实现),这也就是动态规划又被称为查表计算法的原因。
动态规划一般应用于最优化问题(optimization problems)。这类问题一般存在多个解,每个解都具有一个度量值,我们期望得到具有度量值最优(即取最大或最小值)的解。该最优解一般称为最优化问题的一个解。注意,这个解并非唯一,因为最优化问题可能存在多个最优解。

构建一个动态规划算法的一般步骤如下:

  1. 刻画出一个最优解的结构特征(即使用数学表达式来表述一个最优解);
  2. 迭代定义一个最优解的度量值;
  3. 计算每个最优解的度量值,通常采用自下而上的方式;
  4. 根据计算得到的信息构建出原问题的一个最优解。

步骤1-3是使用动态规划求解问题的基础形式。如果我们只需获得最优解的度量值而非最优解本身,则可以忽略步骤4。

三、基于动态规划的问题分析

1
如上图所示,令字符串 s 的长度为M,字符串模式p的长度为N如果s[0, 1, ..., i-1]p[0, 1, ..., j-1]匹配,则令dp[i][j] = true。根据上述定义,可得:dp[0][0] = true表示空字符串完全匹配空字符串,dp[1][1] == true表示p[0]完全匹配s[0]dp[M][N] == true表示p完全匹配s
现在的任务是使用动态规划思想求解迭代表达式dp[i][j](即s[0, 1, ..., i-1]p[0, 1, ..., j-1]的匹配情况),下面分别阐述之:

3.1 p[j - 1] 不等于 ‘*’

2
如上图所示,若p[j - 1] != '*',则p[j - 1]要么是a-z的小写字母中的任意一个,要么是.,于是可得迭代表达式:

dp[i][j] = && dp[i - 1][j - 1] && (s[i - 1] == p[j - 1] || p[j - 1] == '.')

说明:要求i > 0 && i <= M && j > 0 && j <= N,以确保dp[i][j]dp[i - 1][j - 1]取值不越界(下同,不再赘述)。
举例:s[0, ..., i-1] = 'abc'p[0, ..., j-1] = 'abc'p[0, ..., j-1] = 'ab.',可满足上述条件。

3.2 p[j - 1] 等于 ‘*’

p[j - 1] == '*',分为两种情况:

3.2.1 p[j – 2] == s[i – 1]或p[j – 2] == ‘.’

3
如上图所示,迭代表达式为:

dp[i][j] = dp[i][j-2] || dp[i - 1][j - 2] || dp[i - 1][j], if p[j – 2] == s[i – 1] || p[j – 2] == '.'

举例:p[0, ..., j-1] = 'abc*'p[0, ..., j-1] = 'ab.*'可匹配s[0, ..., i-1] = 'abc'(匹配c零次),s[0, ..., i-1] = 'abcc'(匹配c一次), s[0, ..., i-1] = 'abcccc'匹配c多次)。

3.2.2 p[j – 2] != s[i – 1]且p[j – 2] != ‘.’

4
如上图所示,迭代表达式为:

dp[i][j] = dp[i][j-2], if p[j-2] != '.' && p[j-2] != s[i-1] 

举例:p[0, ..., j-1] = 'abc*'可匹配s[0, ..., i-1] = 'ab'

3.3 迭代表达式

综上所述,得到最终的迭代表达式如下:

(1) if p[j - 1] != '*': 
dp[i][j] = dp[i - 1][j - 1] && (s[i - 1] == p[j - 1] || p[j - 1] == '.'), 
(2) if p[j - 1] == '*': 
dp[i][j] = dp[i][j - 2] || ((p[j - 2] == '.' || p[j - 2] == s[i - 1])  &&  
           (dp[i - 1][j - 2] || dp[i - 1][j]))  

四、C++实现代码

使用C++11实现,代码如下:

#include <iostream>
#include <string>
#include <vector>

class Solution {
 public:
  bool IsMatch(const std::string& s, const std::string& p) {
    int m = s.size(), n = p.size();
    std::vector<std::vector<bool>> dp(m + 1, std::vector<bool>(n + 1, false));
    dp[0][0] = true;
    for (int i = 0; i <= m; ++i) {
      for (int j = 1; j <= n; ++j) {
        // The first character shouldn't be '*'
        if (j > 1 && p[j - 1] == '*') {
          dp[i][j] = dp[i][j - 2] ||
                     (i > 0 && (p[j - 2] == '.' || p[j - 2] == s[i - 1]) &&
                      (dp[i - 1][j - 2] || dp[i - 1][j]));
        } else {
          dp[i][j] = i > 0 && dp[i - 1][j - 1] &&
                     (s[i - 1] == p[j - 1] || p[j - 1] == '.');
        }
      }
    }
    return dp[m][n];
  }
};

int main() {
  std::string s1 = "aa";
  std::string p1 = "a";
  std::string s2 = "aa";
  std::string p2 = "a*";
  std::string s3 = "ab";
  std::string p3 = ".*";
  std::string s4 = "aab";
  std::string p4 = "c*a*b";
  std::string s5 = "mississippi";
  std::string p5 = "mis*is*p*.";
  std::string s6 = "test";
  std::string p6 = "*.";
  std::string s7 = "mississippi";
  std::string p7 = "mis*is*ip*.";

  Solution solution;
  std::cout << "Expected: \nfalse, true, true, true, false, false, true"
            << std::endl;
  std::cout << "Actual: \n"
            << std::boolalpha << solution.IsMatch(s1, p1) << ", "
            << solution.IsMatch(s2, p2) << ", " << solution.IsMatch(s3, p3)
            << ", " << solution.IsMatch(s4, p4) << ", "
            << solution.IsMatch(s5, p5) << ", " << solution.IsMatch(s6, p6)
            << ", " << solution.IsMatch(s7, p7) << std::endl;

  return 0;
}

输出结果:

Expected: 
false, true, true, true, false, false, true
Actual: 
false, true, true, true, false, false, true
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值