(推荐)从暴力递归到动态规划--正则表达式的匹配

暴力递归与动态规划

递归的思想是:把原问题转化为规模减小后的同类问题。暴力递归的“暴力”体现在递归过程中会有很多的重复计算,影响算法的效率,而动态规划的方法是把递归过程中重复计算的结果记录下来。

学院派的动态规划的方法在面对实际问题时存在的问题是:不知道该从何入手。其实,那些提出动态规划方法的先贤们,也是先写出暴力递归,进而改写出动态规划的。所以,从暴力递归到动态规划,才是简单可行的路线。

下面,结合一个实例,介绍从暴力递归到动态规划的改写方法。

正则表达式的匹配

题目描述

请实现一个函数用来匹配包括‘.’和‘ * ’的正则表达式。模式中的字符 '.'表示任意一个字符,而‘ * ’表示它前面的字符可以出现任意次(包含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab* ac* a"匹配,但是与"aa.a"和"ab* a"均不匹配

暴力递归的解题思路

首先要考虑两种特殊情况:
(1).当两个字符串都是空时,返回true;
(2).当模式为空,而字符串不为空,返回false;
值得注意的是,当字符串为空,模式不为空的时候,不一定返回false。比如字符串为空,而模式为"b* c* a* "时,‘ * ’前面的字符都为0个,这时字符串和模式是匹配的。
这两种特殊情况称之为“base case”,是递归的基本情况,也是递归开始返回的基本情况。

然后减小问题规模,给出递归解决方法:将整个字符串和整个模式能否匹配的问题转化为字符串某个位置 i i i开始到结尾和模式某个位置 j j j开始到结尾能否匹配的问题。
因为一旦解决了这个问题, i = 0 , j = 0 i=0,j=0 i=0,j=0时就是我们的原始问题。

那么就从 i i i j j j开始匹配两个字符串,结果只有两种:匹配成功(返回true)或者匹配失败(返回false),考虑到 j j j后面位置的那个字符可能是’* ’ ,分成两种情况:
(1).模式 j + 1 j+1 j+1位置上的字符不是‘ * ’,这种情况比较简单,如果 i i i j j j匹配失败,则直接返回false;否则,匹配结果就由字符串从 i + 1 i+1 i+1开始到结尾和模式从 j + 1 j+1 j+1开始到结尾的匹配结果决定了;
这种情况下要注意:模式 j j j上的字符如果是‘.’,字符串 i i i上是任意字符,则匹配成功,但 i i i上不能是’\0’;

(2).模式 j + 1 j+1 j+1位置上的字符是‘ * ’,这时,就要看 ’ * '到底代表0个还是多个:如果‘ * ’代表0个,则匹配结果由字符串从 i i i开始到结尾和模式从 j + 2 j+2 j+2开始到结尾的匹配结果决定;如果‘ * ’代表多个,则匹配结果由字符串从 i + 1 i+1 i+1开始到结尾和模式从 j j j开始到结尾的匹配结果决定(多个和一个的处理方式一样,因为又变成了情况(2) );

c++版的代码如下:

    class Solution {
    public:
        bool match(char* str, char* pattern)
        {        
            return process(str,pattern,0,0);
        }
        
        //递归函数process(),i是str开始匹配位置,j是pattern开始匹配位置
        bool process(char* str, char* pattern, int i, int j)
        {
           //基本情况 base case
            if(str[i]=='\0'&&pattern[j]=='\0')
                return true;
            if(str[i]!='\0'&&pattern[j]=='\0')
                return false;  
            
            //如果pattern[j+1]不是‘*’
            if(pattern[j]!='\0'&&pattern[j+1]!='*'){
                return (str[i]==pattern[j]||(pattern[j]=='.'&&str[i]!='\0'))&&
                    process(str,pattern,i+1,j+1);
            }
            
             //如果pattern[j+1]是‘*’
            else if(pattern[j]!='\0'&&pattern[j+1]=='*'){
           	 if(str[i]!=pattern[j]&&(pattern[j]!='.'||str[i]=='\0'))
               		 return process(str,pattern,i,j+2);  //'*'匹配 0个字符
           	 else{
               		 return process(str,pattern,i+1,j)||process(str,pattern,i,j+2);  //'*'匹配 0个或多个字符                		  
           	 }
            }
            return false;
        }
    };   

从暴力递归到动态规划

暴力递归过程,将 p r o c e s s ( ) process() process()函数看出是一个黑盒,str、pattern参数是固定的,可变参数只有 i i i j j j, i i i的变化范围是 0 ≤ i ≤ s t r l e n ( s t r ) 0\leq i \leq strlen(str) 0istrlen(str), j j j的变化范围是 0 ≤ j ≤ s t r l e n ( p a t t e r n ) 0\leq j \leq strlen(pattern) 0jstrlen(pattern)在这里插入图片描述
所以可以将求 p r o c e s s ( i , j ) process(i,j) process(i,j)的过程看是填一个长为 n 2 n2 n2,宽为 n 1 n1 n1的二维矩阵dp的过程,其中 n 1 = s t r l e n ( s t r ) , n 2 = s t r l e n ( p a t t e r n ) n1=strlen(str),n2=strlen(pattern) n1=strlen(str),n2=strlen(pattern),我们的最终目标是得到dp[0][0]的值是true还是false,也就是红色三角形的位置的值;根据暴力递归的过程我们知道,任意一个dp[i][j]的值依赖于dp[i+1][j]、dp[i+1][j=1]、dp[i][j+2],也就是黑色箭头表示的关系;根据base case,我们知道有一些位置是不需要根据依赖关系就能得到值的,也就是矩阵的最后一列。

一般情况下,有了最终目标、位置依赖关系、base case描述,这道题的动态规划方法就已经确定了。但是这道题有它特殊之处,因为没有最后一行和倒数第二列的已知信息,其他位置就失去了依赖,也就是说暴力递归中的base case是不完整的,虽然不影响暴力递归的求解,但影响了动态规划的求解。

这时就要根据题意确定最后一行和倒数第二列的信息:先看倒数第二列, ( i , j ) (i,j) (i,j) ( n 1 − 1 , n 2 − 1 ) (n1-1,n2-1) (n11,n21)时,表示都只有一个字符,此时可能匹配成功,可能匹配失败,根据给定的str和pattern确定,而倒数第二列的其他位置肯定都是false,因为pattern只有一个字符,而str不止一个字符;再看最后一行,str为“\0”,那么pattern要跟str匹配成功只能是“a* b* c*”这种形式,也就是“?”位置可能匹配成功,其余位置肯定匹配失败。

将base case全部填好之后,剩下的工作就是根据依赖关系从base case出发填好整个表,那么目标位置的结果自然也得到了,这就是动态规划,像搭积木一样的过程。

c++版本代码如下:

    class Solution {
    public:
        bool match(char* str, char* pattern)
        {
            int n1=strlen(str);
            int n2=strlen(pattern);
            
            //申请dp矩阵空间
            bool** dp;
            dp = new bool *[n1+1];
            for (int i = 0; i < n1+1; i++){
                dp[i] = new bool[n2+1];
            } 
             
            //base case的填写
            dp[n1][n2]=true;
            for(int i=n2-2;i>-1;i-=2){
                if(pattern[i]!='*'&&pattern[i+1]=='*')
                    dp[n1][i]=true;
                else
                    dp[n1][i]=false;
            }
            for(int i=0;i<n1;i++)
            {
                dp[i][n2]=false;
                n2>0&&(dp[i][n2-1]=false);
            }
            if (n1>0&&n2>0) {
                if (pattern[n2-1]=='.'|| str[n1-1]==pattern[n2-1]) {
                    dp[n1-1][n2-1]=true;
                }
            }
            
            //根据依赖关系填写dp矩阵
            for(int i=n1-1;i>-1;i--)
                for(int j=n2-2;j>-1;j--){
                        if(pattern[j]!='\0'&&pattern[j+1]!='*'){
                            dp[i][j]=(str[i]==pattern[j]||(pattern[j]=='.'&&str[i]!='\0'))&&
                                dp[i+1][j+1];
                        }
                        else if(pattern[j]!='\0'&&pattern[j+1]=='*'){
                            if(str[i]!=pattern[j]&&(pattern[j]!='.'||str[i]=='\0'))
                                dp[i][j]=dp[i][j+2];
                            else{
                                dp[i][j]=dp[i+1][j]||dp[i+1][j+2]||dp[i][j+2];
                            }
                        }
                }
            
            //保存结果
            bool res=dp[0][0];
            
            //释放内存
            for (int i = 0; i < n1+1; i++){
                delete[] dp[i];
            }
            delete[]dp;
             
            return res;
        }
    };

总结

其实,从这个例子可以发现,从暴力递归到动态规划的路线,难点在于暴力规划的实现,而改写动态规划的过程是极具套路性的,甚至已经和原题意没有什么关系了。

希望大家多多总结,如果有错漏请指正。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值