LeetCode 10. Regular Expression Matching

题目

Given an input string (s) and a pattern (p), implement regular expression matching with support for '.' and '*'.

'.' Matches any single character.
'*' Matches zero or more of the preceding element.

The matching should cover the entire input string (not partial).

Note:

  • s could be empty and contains only lowercase letters a-z.
  • p could be empty and contains only lowercase letters a-z, and characters like . or *.

Example 1:

Input:
s = "aa"
p = "a"
Output: false
Explanation: "a" does not match the entire string "aa".

Example 2:

Input:
s = "aa"
p = "a*"
Output: true
Explanation: '*' means zero or more of the preceding element, 'a'. Therefore, by repeating 'a' once, it becomes "aa".

Example 3:

Input:
s = "ab"
p = ".*"
Output: true
Explanation: ".*" means "zero or more (*) of any character (.)".

Example 4:

Input:
s = "aab"
p = "c*a*b"
Output: true
Explanation: c can be repeated 0 times, a can be repeated 1 time. Therefore, it matches "aab".

Example 5:

Input:
s = "mississippi"
p = "mis*is*p*."
Output: false

这道题是某公司的某万年面经题。这道题有两个解法,递归或者dp。先开始学习了dp的解法:

令dp[i][j]表示s[0, ..., i - 1]和p[0, ..., j - 1]是否match,注意到是dp[i][j]是匹配到i -1和j - 1,因此我们的dp矩阵的行列数目是比原始数组多1的。那么我们可以得到dp矩阵如下:

         p[0] p[1] p[2] ...
    s[0]  1    0    0
    s[1]  0
    s[2]  0

注意我们的dp[0][0]表示的是s和p均为空的情况,这种情况下一定是match的,所以dp[0][0]为true。当p为空时,只有s为空才能match,因此第一列应当全部为false,而当s为空时,p可能可以以".*"的形式存在,因此需要后面在遍历矩阵的时候判断。

接下来我们按行遍历矩阵,两个for循环,外层的负责循环s[i]行,内层的负责循环p[j]列,因为第0列已经不需要再进行操作,因此j可以从1开始遍历。接下来就是分情况讨论的部分了:

1. p[j - 1] != '*':

如果p[j - 1](也就是最后一个字符)不为*的话,这种情况比较简单,直接就是dp[i -1][j - 1] && (p[j - 1] == '.' || s[i - 1] == p[j - 1]),也就是最后一个字符要匹配(或者p为.)且前面的字符都匹配;

2. p[j - 1] == '*':

如果p[j - 1]为*的话,就比较复杂了。对于*的情况,它可以匹配前一个字符出现了一次或多次,或者前一个字符没有出现。这里又是两种情况:

2.1:匹配一次或多次

假如s="abc", p="abc*",那么我们需要保证,s[i - 2]结尾的(ab)substring要和p[i - 1]结尾的(ab)substring匹配,即dp[i - 1][j] == true,并且s的最后一个字符要和p在*前面的字符匹配,也就是s[i - 1] == p[j - 2]或p[j - 2] == '.'

2.2:匹配空

假如s="abc", p="abcd*",那么我们直接考虑以*前面的前面的字符结尾的字符串是否与s相匹配,也就是dp[i][j - 2]是否为true

分情况讨论完以后代码就很好写出来了,主要需要注意的地方就是,出现i - 1的时候要小心i为0的情况。

代码如下,时间复杂度我想应该是O(mn),其中m是s的长度,n是p的长度,空间复杂度也是。运行时间8ms,64.76%,空间8.8M,69.49%:

class Solution {
public:
    /* 
    dp[i][j]: s[0, ..., i - 1] match p[0, ..., j - 1]
    if (p[j - 1] != '*'):
        dp[i][j] = dp[i-1][j-1] && (s[i - 1] == p[j - 1] || p[j - 1] == '.')
    if (p[j - 1] == '*'):
        if * indicates 0 times:
            dp[i][j] = dp[i][j-2]
        if * indicates 1 or more times:
            dp[i][j] = dp[i-1][j] && (s[i-1] == p[j-2] || p[j-2] == '.')
        p[0] p[1] p[2] ...
    s[0]  1    0    0
    s[1]  0
    s[2]  0
    ...
    */
    bool isMatch(string s, string p) {
        // s.size() + 1 rows
        // p.size() + 1 cols
        vector<vector<bool>> dp(s.size() + 1, vector<bool>(p.size() + 1, false));
        dp[0][0] = true;
        
        for (int i = 0; i < dp.size(); i++) {
            // if p is empty, only empty s can match it
            // traverse j starting from 1 (traverse p for a specific s)
            for (int j = 1; j < dp[0].size(); j++) {
                if (p[j - 1] != '*') {
                    dp[i][j] = i && dp[i - 1][j - 1] && (s[i - 1] == p[j - 1] || p[j - 1] == '.');
                }
                else {
                    dp[i][j] = dp[i][j - 2] || (i && dp[i-1][j] && (s[i-1] == p[j-2] || p[j-2] == '.'));
                }
            }
        }
        return dp[s.size()][p.size()];
    }
};

另一种方法是递归的方法,其实递归和dp还挺像的,分情况讨论都是一样的情况,只是dp考虑以某个字符为结尾的字符串,递归考虑以某个字符为开始的字符串。

对于递归,我们对于p的长度进行讨论,分以下几种情况(有的情况可以合并,但是我感觉合并以后不够直观,就不合并了):

1. p.size() == 0:那么直接返回s.size() == 0,只有空字符串能匹配,这个可以作为base case

2. p.size() == 1:那么只有当s.size() == 1,且p[0]和s[0]匹配的情况下才能匹配(p[0] == s[0] || p[0] == '.')

3. p.size() > 1:需要考虑第二个字符是否是*

3.1 p[1] != '*',那么要求p[0]和s[0]要match,并且p[1]开始的substring要和s[1]开始的substring match

3.2 p[1] == '*',那么还是分为两种情况,要么*表示匹配空,要么表示匹配一次或多次:

3.2.1 匹配一次或多次:s="abc", p="a*bc",那么要求s[0] == p[0](匹配一次嘛)并且s[1]开始的substring要和p继续match(p保持不变是因为可能可以match到0次的)

3.2.2 匹配空:s="abc",p="z*abc",那么要求s要match p[2]开始的substring

递归的时空复杂度比较复杂,暂且先略过……代码跑出来的时间208ms,16.52%,空间15.2M,11.87%,可以说是完全被dp比下去了。

class Solution {
public:
    bool isMatch(string s, string p) {
        if (p.empty()) {
            return s.empty();
        }
        bool first_match =  p[0] == s[0] or p[0] == '.';
        if (p.size() == 1) {
            return s.size() == 1 && first_match;
        }
        else {
            if (p[1] != '*') {
                return !s.empty() && first_match && isMatch(s.substr(1, s.size()), p.substr(1, p.size()));
            }
            else {
                return isMatch(s, p.substr(2, p.size())) || (!s.empty() && first_match && isMatch(s.substr(1, s.size()), p));
            }
            // wrong:
            // else {
            //   if (!s.empty() && first_match) {
            //     return isMatch(s.substr(1, s.size()), p);
            //   }
            //   return isMatch(s, p.substr(2, p.size()));
            // }
      }
    }
};

注意到上面的代码中,最后我注释掉了一些代码,这样做会导致类似于"aaa" "a*a"的test case跑不过去,具体原因没有细究,但感觉就是这两个条件之间的关系非常微妙,如果不符合前一个应该可以考虑后一个,而如果单独拎一个if写出来可能就不太对了?


然后下面来讲一下某公司的面经题,和上面这道lc差不多,改动/加了以下几个条件:

1. 每次匹配的时候不是匹配整个字符串,而是匹配字符串中的任意一个子字符串

2. ?(其实就是*)代表的只能是前一个字符出现了零次或一次

3. ?不能出现在pattern的第一位,且pattern中不能有连续两个?合在一起

这道题的思路和上面的相似,也采用递归的方式实现,但是由于是部分匹配,因此需要先找出首字母匹配的子字符串,然后分别对这些子字符串进行递归判断。为了准备面试,还是从头说起思路。

general idea就是,我们可以遍历整个字符串,找到和pattern的首字符匹配的位置,然后对以这个字符为起始的子字符串进行判断是否match。为了判断子字符串是否匹配,我们可以采用递归的方式实现。

首先我们先排除一些特殊情况,比如s和p均为空、或者p[0] == '?'的情况。然后我们遍历整个s字符串,如果发现当前的字符和p[0] match(包括相等和p[0] == '.'),则开始进行递归判断是否match。

对于递归条件的分类,和上面的相似,我们按照p的长度分为以下几类:

0. p.size() == 0作为base case:说明该匹配的已经匹配完了,返回true

1. p.size() == 1作为base case:如果p的长度只有1,那么我们需要保证p[0]和s[0] match

2. p.size() > 1,那么可能有*的出现:

2.1 p[1] != '*':说明p[0]和s[0]要match,并且从p[1]和s[1]开始的子字符串也要match

2.2 p[1] == '*':说明出现了*,那么p[0]一定不能是'*'(题目规定)

2.2.1 考虑*表示前一个字符出现一次的情况,比如s="abc", p="a*bc",那么我们需要保证s[0] == p[0],且s[1]开始的substring和p[2]开始的substring要match

2.2.2 考虑*表示前一个字符不出现的情况,比如s="abc", p="d*abc",那么我们需要保证s和p[2]开始的substring要match

代码大概如下,也许或许是没啥问题的:

#include <iostream>
using namespace std;

// To execute C++, please define "int main()"

bool isMatch1(string s, string p) {
  for (int i = 0; i < s.size(); i++) {
    if (s[i] == p[0] || p[0] == '.') {
      int j;
      for (j = 1; j < p.size(); j++) {
        if (s[i + j] == p[j] || p[j] == '.') {
          continue;
        }
        else {
          break;
        } 
      }
      if (j == p.size()) {
        return true;
      }
    }
  }
  return false;
}


bool helper(string s, string p) {
  // if p.size() == 0, return true;
  // if p.size() == 1, p[0] == s[0] or p[0] == '.'
  // if p.size() > 1, 
  // p: a?bc; s: bc
  // 1. if p[1] != '?', p[0] == s[0] or p[0] == '.' && helper(s.substr(1), p.substr(1))
  // 2. if p[1] == '?', 
  // 1 not here: helper(s, p.substr(2)) 
  // if(1 ==1|| p[0]=='.') helper(s.substr(1), p.substr(2))
  if (p.size() == 0) {
    return true;
 }
  bool first_match =  p[0] == s[0] or p[0] == '.';
  if (p.size() == 1) {
    return first_match;
  }
  else {
    if (p[1] != '?') {
      return !s.empty() && first_match && helper(s.substr(1, s.size()), p.substr(1, p.size()));
    }
    else {
        return p[0] != '?' && (!s.empty() && first_match && helper(s.substr(1, s.size()), p.substr(2, p.size())) || helper(s, p.substr(2, p.size())));
    }
  }
}


bool isMatch(string s, string p) {
  if (p.empty()) {
    return s.empty();
  }
  if (p[0] == '?') {
    return false;
  }
  for (int i = 0; i < s.size(); i++) {
    if (s[i] == p[0] || p[0] == '.') {  // if "a?" matches "hello world", delete the if
      if (helper(s.substr(i, s.size()), p)) {
        return true;
      }
    }
  }
  return false;   
}


int main() {
  cout<< isMatch("hello world","hello") << endl; //1
  cout<< isMatch("hello world","HELLO") << endl; //0
  cout<< isMatch("hello world","worl") << endl;  //1
  cout<< isMatch("hello world","o wor") << endl; //1
  cout<< isMatch("hello world","o.wor") << endl; //1
  cout<< isMatch("hello world",".ll") << endl;  //1
  cout<< isMatch("hello world","hh?ello") << endl; //1
  cout<< isMatch("hello world","wo?rld") << endl; //1
  cout<< isMatch("hello world",".?ello") << endl; //1
  cout<< isMatch("hello world",".??") << endl; //0
  cout<< isMatch("hello world","?word") << endl; //0
  cout<< isMatch("hello world","hel?o") << endl; //0
  cout<< isMatch("hello world",".. w?o") << endl; //1
  cout<< isMatch("hello world",".?.?.?ld") << endl; //1
  cout<< isMatch("hello world","ee?llo") << endl; //1
  cout<< isMatch("hello world","h?llo") << endl; //0
  cout<< isMatch("hello world","h?") << endl; //1
  cout<< isMatch("hello world","e?") << endl; //1
  cout<< isMatch("hello world"," ?") << endl; //1
  cout<< isMatch("hello world",".?") << endl; //1
  cout<< isMatch("hello world","a?") << endl; //1 or 0?
  string s2 = "ab";
  string p6 = ".?c";
  cout << isMatch(s2, p6) << endl;  // 0
  string s3 = "aa";
  string p7 = "a?";
  cout << isMatch(s3, p7) << endl;  // 0
  string s4 = "aaa";
  string p8 = "a?a";
  cout << isMatch(s4, p8) << endl;  // 1
  return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智慧校园建设方案旨在通过融合先进技术,如物联网、大数据、人工智能等,实现校园的智能化管理与服务。政策的推动和技术的成熟为智慧校园的发展提供了基础。该方案强调了数据的重要性,提出通过数据的整合、开放和共享,构建产学研资用联动的服务体系,以促进校园的精细化治理。 智慧校园的核心建设任务包括数据标准体系和应用标准体系的建设,以及信息化安全与等级保护的实施。方案提出了一站式服务大厅和移动校园的概念,通过整合校内外资源,实现资源共享平台和产教融合就业平台的建设。此外,校园大脑的构建是实现智慧校园的关键,它涉及到数据中心化、数据资产化和数据业务化,以数据驱动业务自动化和智能化。 技术应用方面,方案提出了物联网平台、5G网络、人工智能平台等新技术的融合应用,以打造多场景融合的智慧校园大脑。这包括智慧教室、智慧实验室、智慧图书馆、智慧党建等多领域的智能化应用,旨在提升教学、科研、管理和服务的效率和质量。 在实施层面,智慧校园建设需要统筹规划和分步实施,确保项目的可行性和有效性。方案提出了主题梳理、场景梳理和数据梳理的方法,以及现有技术支持和项目分级的考虑,以指导智慧校园的建设。 最后,智慧校园建设的成功依赖于开放、协同和融合的组织建设。通过战略咨询、分步实施、生态建设和短板补充,可以构建符合学校特色的生态链,实现智慧校园的长远发展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值