10 正则表达式匹配

困难题
在这里插入图片描述

一道动态规划算法题。虽然之前就了解过动态规划,但不能熟练应用,因此再将其要点先罗列一下,再做题目。

动态规划

动态规划是利用历史记录来避免重复计算,类似数学归纳法,后项的状态能根据前项的状态递推出来。历史记录需要用数据结构保存,一般采用数组(一维,二维……)。主要步骤分为三步:

  1. 确定数组元素意义,通常是某个状态。这是相当关键的一步,代表了对这道题的理解程度,可以算是常说的将“实际问题转化成数学问题”。在此题中,有字符串s和模式p,因此需要使用二维数组dp[i][j]。数组元素的含义就变成:长度为j的模式匹配长度为i的字符串的状态。值为true,表示可以匹配;值为false,表示不能匹配。
  2. 找到递推表达式。通常来说,就是假设数组i之前的元素值都已知,如何递推第i项的值。这一般是动态规划题的难点。
  3. 找到初始值,这个通常能直接看出或通过非常简单的讨论可以得出。

题目入手(确定元素意义)

字符串匹配的过程完全符合动态规划过程,即:当前字符能否匹配取决于前面是否匹配和当前字符能否匹配。既然有字符串s和模式p存在,用一维数组肯定不够表示状态,所以要用二维数组。状态是什么,自然就是题目的结论——字符串和模式的匹配情况。一般情况就是字符串前i项和模式前j项能否匹配,最终情况能得到字符串和模式能否匹配。

递推表达式

假设我们已知当前字符串的前i-1项和模式的前j-1项的匹配情况(注意:只要是项数小于i,j的,我们都应该是已知的。这里的已知并不代表“能匹配”,而是代表已知“能否匹配”),需要递推到dp[i][j]的能否匹配。我们可以先来一点简单的熟悉一下:

s = “abcd”, p=“abcd”。i和j都在3的位置,即都指向d。
这时s和p的末尾两个字符都是d,能匹配,所以dp[3][3]能否匹配取决于dp[2][2],即dp[3][3] = dp[2][2]。如果dp[2][2]能匹配,那么dp[3][3]也能匹配,否则就不能匹配。

通过这个例子,我们发现dp[i][j]的状态需要观察当前正在匹配的字符是什么,即s[i]和p[j]。然后就可以开始进行复杂一些的分类讨论。当然在讨论中,我们先把简单的讨论清楚。

1.单字符直接匹配。表达式为s[i] == p[i] || p[i] == ‘.’。出现这种情况,说明这一位的字符能够匹配模式的一位字符。那么dp[i][j] = dp[i-1][j-1]。 (举例:s=“abcd”,p=“abcd”)
2. 出现‘*’号。表达式为p[i] == ‘*’。‘*’是匹配‘*’前的字符0次或者多次。但是我们不知道到底匹配了没有,因此要分类讨论。
2.1 没法匹配。表达式为p[i-1] != s[i] && p[i-1] != ‘.’。如果是这种情况,那dp[i][j] = dp[i][j-2],即模式往前推两个字符。(举例:s=“abcd”, p=“abcf*”,标记部分本身无法匹配,那么dp[3][4] = dp[3][2])
2.2 能够匹配。诶,这里就出现了一个点。能够匹配,我一定要匹配吗?我可不可以和上面一样,照样不匹配?当然是可以的。所以这里要继续分类讨论。
2.2.1 选择不匹配。选择不匹配有点类似2.1,只不过这次s[i]和p[i-1]能够匹配。如果是这种情况,同2.1一样,dp[i][j] = dp[i][j-2]。(举例:s=“abcd”,p=“abcdd*”,虽然这个d*可以匹配s[3]的d,但是也可以选择不匹配。那么dp[3][5] = dp[3][3])
2.2.2 选择匹配一个。这次我们要匹配一个s,我们通过一个例子看一下会发生什么。(举例:s=“abdd”,p="abd*,我们将s[3]的d匹配了p[4]的d*后,又要考虑两种情况:第一,就当作正好匹配完成,那么dp[3][3] = dp[2][1];第二,只匹配一个,保留这个模式以匹配后面的,那么dp[3][3] = dp[2][3]。这两种情况都被认可,但是仔细考虑,我们这里可以省去第一种情况,如果在这里我们省去,那么这一种情况会匹配到情况二,并可能在某一次循环中最终匹配到情况2.1)
2.2.3 选择匹配多个。那么这一条就不必要了,因为匹配多个也是需要一个个匹配的,这个会反复循环2.2.2的第二个做法。

初始状态

初始状态即考虑dp[0][0]的状态。由于题目有规定,“*”号一定是有字符的,所以不会出现在p的第一个,所以这里只要判断一下就可以了。但是如果就这么写的话,我们的循环就不能简单地从0开始,因为循环中dp[i][j]会往前看,导致前方溢出。一种做法当然是从1开始循环,但是需要做一些初始化工作。另一种做法是增加哨兵元素。在字符串s和模式p串的开头都增加一个空字符,这样不会改变匹配结果,也不用担心循环溢出了,只是在做递推表达式判断的时候要注意i和j的意义。

最终代码

#include <iostream>
#include <string.h>

using namespace std;

class Solution {
public:
    bool isMatch(string s, string p) {
        s=" "+s;//防止该案例:""\n"c*"
        p=" "+p;
        int m=s.size(),n=p.size(); //现在的字符串s和模式p都比原来多1
        bool dp[m+1][n+1]; //都要增加1,所以最终要看d[m][n]
        memset(dp,false,(m+1)*(n+1));
        dp[0][0]=true; 
        //dp[i][j] = true 表示:s串前i个字符能被模式p的前j的部分匹配
        //false当然就表示,不能匹配

        //为了更好地说明,直接上例子。假设s = "aaaa", p = "aaa*", 那么增加空字符后s = " aaaa", p = " aaa*", 且m = 5, p = 5
        //对应的dp结构如下:
        
        //p\s    # a a a a
        //  dp 0 1 2 3 4 5
        //   0 t f f f f f
        // # 1 f f f f f f
        // a 2 f f f f f f
        // a 3 f f f f f f 
        // a 4 f f f f f f
        // * 5 f f f f f f
        
        //其中,#代表空字符。可以看到,dp[0][0]这个哨兵本身没有意义,但是在循环里可以用来初始化dp[1][1]。
        //因此产生的对应关系是,dp[i][j]如果是1,实际上表示[0,i-1]的字符s能被[0.j-1]的模式p匹配。

        for(int i=1;i<=m;i++){
            for(int j=1;j<=n;j++){
                //从dp[1][1](对于字符串和模式来说即第一个非空字符)更新到d[m][n]的状态,每次更新d[i][j],这里的d[i][j]对应新字符串s[i-1],p[j-1]
                //从前往后,因此前面的状态必然都被更新过了,所以可以实现状态转移方程
                if(s[i-1]==p[j-1] || p[j-1]=='.'){
                    //s[i-1]和p[j-1]相等(其实这里默认是字符,因为s里不会出现非字符),或不等,但p[j-1]是任意字符'.'
                    dp[i][j]=dp[i-1][j-1]; //匹配一个字符,成功
                }
                else if(p[j-1]=='*'){
                    //出现'*'
                    if(s[i-1]!=p[j-2] && p[j-2]!='.')
                        //'*'和前面的字符不能匹配s[i-1],那只好丢弃这两个位
                        dp[i][j]=dp[i][j-2];
                    else{
                        //'*'和前面的字符可以匹配s[i-1](可能是正好相同,也可能是任意字符'.')
                        //这个或运算的理解是,三种情况举例如下:
                        //1. s = "aa", p = "aaa*", 虽然最后一个a能匹配a*, 但这里还是选择丢弃a*。
                        //2. s = "aaaa", p = "aaa*", 多字符匹配,s最后一个a匹配p的a*,但考虑到后面可能还要用到a*,所以a*保留。
                        //这两种情况任一种成功都算作匹配成功,接下来可以继续匹配。
                        dp[i][j] = dp[i][j-2] || dp[i-1][j];
                    }
                }
            }
        }
        return dp[m][n]; //全部匹配完,最后的结果存储在dp[m][n]
    }
};

int main(){
    Solution st;

    cout<<st.isMatch("a","a*");

    return 0;
}

总结

一上来做这种难度的动态规划也有点超纲,但是静下心来仔细分析的话还是能够收获不少的,仔细想想也挺中规中矩的,不是那么难以接受。总之这三步先搞懂他这个理念,细节一开始写不出来可以看着题解先做。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值