[leetcode日记]44.通配符匹配

今天的每日一题是21.合并两个有序链表。5min就过了,90%+100%.换了递归做居然也是这个结果,感觉那道题写博客也没什么意思。这是升级版的博客链接:[leetcode日记]23.合并k个链表
所以我就又随机选了一道,就是今天这题。

题目

给定一个字符串 (s) 和一个字符模式 § ,实现一个支持 ‘?’ 和 ‘*’ 的通配符匹配。

‘?’ 可以匹配任何单个字符。 ‘*’ 可以匹配任意字符串(包括空字符串)。 两个字符串完全匹配才算匹配成功。

说明:

s 可能为空,且只包含从 a-z 的小写字母。 p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。 示例 1:

输入: s = “aa” p = “a” 输出: false 解释: “a” 无法匹配 “aa” 整个字符串。 示例 2:

输入: s = “aa” p = "" 输出: true 解释: '’ 可以匹配任意字符串。 示例 3:

输入: s = “cb” p = “?a” 输出: false 解释: ‘?’ 可以匹配 ‘c’, 但第二个 ‘a’ 无法匹配 ‘b’。
示例 4:

输入: s = “adceb” p = “ab” 输出: true 解释: 第一个 ‘’ 可以匹配空字符串, 第二个 '
可以匹配字符串 “dce”. 示例 5:

输入: s = “acdcb” p = “a*c?b” 输入: false

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/wildcard-matching
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

很不幸随机到了一道hard,而且还是我不太擅长的字符串操作。
读完题目以后第一个反应就是动态规划,主要有这么几种情况要讨论:

  1. if(p[i]==s[j]) ,dp[i][j] =dp[i-1][j-1]
  2. if(p[i] == ‘?’),dp[i][k] = dp[i-1][k-1]
  3. if(p[i]==’*’),dp[i][j] = dp[i-1][j-1] || dp[i][j-1] || dp[i-1][j];
  4. 初始边界处理,特别要注意当p是以若干个连续的‘*’开头的情况

初步代码如下:

bool isMatch(char * s, char * p){
    int len_s = strlen(s), len_p = strlen(p);
    
    int** dp = (int**)malloc(sizeof(int*)*(len_s+1));
    for (int i = 0; i <= len_s; ++i){
        *(dp+i) = (int*)malloc(sizeof(int)*(len_p+1));
        memset(*(dp+i), 0, sizeof(int)*(len_p+1));
    }

    dp[0][0] = 1;
    for (int i = 1; i <= len_p; ++i){ 
        // 考虑边界条件,p前面若干位为*就在对应位置赋值1,当p出现非*字符时跳出
        if (p[i-1] == '*')
            dp[0][i] = 1;
        else
            break;
    }

    for (int i = 1; i <= len_s; i++){
        for (int j = 1; j <= len_p; j++){
            if (s[i-1] == p[j-1])
                dp[i][j] = dp[i-1][j-1];
            else{
                if (p[j-1] == '?')
                    dp[i][j] = dp[i-1][j-1];
                else if (p[j-1] == '*')
                    dp[i][j] = dp[i-1][j-1] || dp[i][j-1] || dp[i-1][j];
            }
        }
    }

    return dp[len_s][len_p];
}

运行结果:

在这里插入图片描述

效率相当一般。肯定还存在优化空间。

首先是动态规划空间的申请,因为其实变量里面只存储true和false,因此完全不需要用int,改用bool就可以从4字节降到3字节。

其次如果使用calloc来申请空间,就不需要调用memset函数了(这个优化效果应该微乎其微)

最后我尝试了一下先动p的下标,再动s的下标,发现多了一种可以快速判断的情况:当p结尾是*的情况下,可以直接把该位所有对应下标置true,否则直接跳出循环(不去改变数值就是保持为false)。
感觉说的可能还没有代码清楚。这种情况的代码块如下

if(p[p_index] == '*') {
    for (s_index = 0; s_index < s_len + 1; s_index++) {
        if(dp[p_index][s_index]) {
            for (; s_index < s_len + 1; s_index++) {
                dp[p_index + 1][s_index] = true;
            }
            break;
        }
    }
}

优化动态规划代码

bool isMatch(char * s, char * p){
    int s_len = strlen(s);
    int p_len = strlen(p);
    if (p_len <= 1){
        if (s_len == 0 && p_len == 0) return true;  //两者均空
        else if (p[0] == '*') return true;   //p为单字符‘*’
        else if (s_len != 1) return false;  //s长度大于1
        else if (p[0] == '?' || s[0] == p[0]) return true;  //都为1位且匹配
    }
    // s做行,p做列
    bool **dp = (bool **) calloc(p_len + 1, sizeof(bool *));
    int i = 0;
    for (i = 0; i < p_len + 1; i++) {
        dp[i] = (bool *) calloc(s_len + 1, sizeof(bool));
    }
    dp[0][0] = true;
    int s_index = 0, p_index = 0;
    for (p_index = 0; p_index < p_len; p_index++) {
        if(p[p_index] == '*') {
            for (s_index = 0; s_index < s_len + 1; s_index++) {
                if(dp[p_index][s_index]) {
                    for (; s_index < s_len + 1; s_index++) {
                        dp[p_index + 1][s_index] = true;
                    }
                    break;
                }
            }
        } else if (p[p_index] == '?') {
            for (s_index = 0; s_index < s_len; s_index++) {
                dp[p_index + 1][s_index + 1] = dp[p_index][s_index];
            } 
        } else {
            for (s_index = 0; s_index < s_len; s_index++) {
            //把s[s_index]==p[p_index]的if判断改成用&&连接
                dp[p_index + 1][s_index + 1] = dp[p_index][s_index] && (s[s_index] == p[p_index]);
            }
        }
    }
    return dp[p_len][s_len];
}

优化后运行结果:

在这里插入图片描述

不得不说,用时和内存消耗都明明减半了,一个排名翻倍了,一个排名一个点都没有动,真是太奇怪了。

空间还是申请了太多,还有没有什么好办法?动态规划空间开销几乎是固定的那么多,还能优化吗?
我注意到其实生成p_index的那一整行的下标时,我其实只用到了p_index - 1 那一行,而且最后也只用返回最后一行的值,不需要返回匹配的方式意味着我不需要回溯,可以把用过的数据覆盖掉!也就是说,我完全可以只申请两行空间然后交替使用——就像桐人老爷和爱丽丝两个人爬塔一样,用两根棍子,一根垫脚、一根用于向上攀登,当用于攀登的棍子沦为垫脚后就把原来垫脚的那根回收,并再次将它作位向上攀登用的新棍子——虽然我只有两行空间,但是合理利用可以达到够用的目的!交替使用的办法就是把行号改成对2取余数。好吧,我承认之前那个桐人老爷的例子不够生动,换种说法吧,比如要过一条河,只有两块木板,怎么过?之前正常方法就是木板一路铺过去就能过河,现在我每走到一块木板上,就得把后面的木板抽出来放在前面,以此类推,没错就是这个意思,如果还没解释清楚的话就请看代码吧…

我修改了代码,尝试了一下发现会产生一个新的bug:那就是我之前为了用break跳出了循环(因为后面位默认是0),现在因为有一个覆盖重写的问题导致后面那几位可能不是0,产生结果将一些false输出为true。所以我需要每个循环重写每一位。(这个开销应该不算大)

两块木板过河_究极省空间的动态规划

bool isMatch(char * s, char * p){
    int s_len = strlen(s);
    int p_len = strlen(p);
    if (p_len <= 1){
        if (s_len == 0 && p_len == 0) return true;  //两者均空
        else if (p[0] == '*') return true;   //p为单字符‘*’
        else if (s_len != 1) return false;  //s长度大于1
        else if (p[0] == '?' || s[0] == p[0]) return true;  //都为1位且匹配
    }
    // s做行,p做列
    bool **dp = (bool **) calloc(2 , sizeof(bool *));
    int i = 0;
    for (i = 0; i < 2 ;i++) {
        dp[i] = (bool *) calloc(s_len + 1, sizeof(bool));
    }
    dp[0][0] = true;
    int s_index = 0, p_index = 0;
    for (p_index = 0; p_index < p_len; p_index++) {
    	if (p_index >= 1) dp[(p_index+1)%2][0] = dp[(p_index)%2][0] && (p[p_index] == '*');
        // putchar('\n');
        if(p[p_index] == '*') {
            for (s_index = 0; s_index < s_len + 1; s_index++) {
               if(dp[p_index%2][s_index]) {
                    for (; s_index < s_len + 1; s_index++) {
                        dp[(p_index + 1)%2][s_index] = true;
                        // printf("%d,",dp[(p_index + 1)%2][s_index]);
                    }
                    break;
                }else {
                    dp[(p_index + 1)%2][s_index] = false;
                    // printf("%d,",dp[(p_index + 1)%2][s_index]);
                }
            }
        } else if (p[p_index] == '?') {
            for (s_index = 0; s_index < s_len; s_index++) {
                dp[(p_index + 1)%2][s_index + 1] = dp[p_index%2][s_index];
                // printf("%d,",dp[(p_index + 1)%2][s_index + 1]); 
            } 
        } else {
            for (s_index = 0; s_index < s_len; s_index++) {
            //把s[s_index]==p[p_index]的if判断改成用&&连接
                dp[(p_index + 1)%2][s_index + 1] = dp[p_index%2][s_index] && (s[s_index] == p[p_index]);
                // printf("%d,",dp[(p_index + 1)%2][s_index + 1]);
            }
        }
    }
    return dp[p_len%2][s_len];
}

没错,就是这样!运行结果的空间开销令我满意:
在这里插入图片描述
看到这个结果,我真是感动得泪流满面,因为我调试了大半天…请看图:
在这里插入图片描述
可能看起来我改的代码不多,但是化身为bug斗士两小时的我眼泪流下来……这一切只为了实现一个两块木板过河的动态规划.说一下最关键的一步吧:

if (p_index >= 1) dp[(p_index+1)%2][0] = dp[(p_index)%2][0] && (p[p_index] == '*');

就是这行命令,因为之前如果不强行省空间的话只需要dp[0][0]=1,就可以了,其他行默认就有dp[*][0] = 1,用两块木板的话,dp[2k+1][0] 默认就是1了……就是为了找&&改好这个bug,我费了九牛二虎之力。当然还有其他bug,这个是我认为最难找也最难改的一个。
看到这里的小伙伴,应该就算是出于同情也该给我点个赞吧……

好了,以上就是动态规划方法了,我认为我已经优化到了极致,那有没有其他办法解决这个问题呢?在拟写解答的过程中,我就注意到了一个方法:深度优先搜索。
需要回溯的地方只有通配符出现的时候,用链个变量记录连续若干个 * 的最后一个的时候两个变量各自的位置,当一次深度优先搜索失败的时候就回溯到这个变量指示的位置。又是到了我最喜欢的名言:

talk is cheap,show me the code

深度优先搜索

bool isMatch(char * s, char * p){
    int j = 0;
    int start = 0;
    int last = 0;
    for(int i = 0; i < strlen(s);) {
        if(j < strlen(p) && (s[i] == p[j] || p[j] == '?')){
            i++;
            j++;
        } else if(j < strlen(p) && p[j] == '*') {
            last = i;
            start = ++j;
        } else if(start != 0){
            i = ++last;
            j = start;
        } else {
            return false;
        }
    }
    for(; j<strlen(p)&& p[j]=='*'; ++j);
    return j==strlen(p);
}

哇,这长度,这空间,不用跑就知道,结果一定很棒!

放弃动态规划以后的运行结果

在这里插入图片描述

妈妈咪啊,结果比我想象得还要好……深刻诠释了什么叫做动态规划从入门到优化到debug到放弃。

朋友,都看到这里了,难道不愿意给在这道题目上死磕了半天的我点一个小小的赞嘛~

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邵政道

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值