44. 通配符匹配(从暴力递归到动态规划)

news2025/4/6 14:16:45

题目链接:力扣

 所有的动态规划都可以使用暴力递归求解,如果推导dp方程比较困难,可以先使用暴力递归进行尝试,然后将从递归改为动态规划,这种方式在dp方程求解困难的情况下非常有效,而且从递归修改为动态规划时比较容易求解dp初始值以及dp的更新方式。

暴力递归解法:

  1. 题目是两个字符串匹配的过程,可以使用递归分解成子问题来做,递归求解的关键在于定义递归函数的含义,以及递归的终止条件
  2. 递归函数的定义:boolean process(char[] s,char[] p,int sL,int pL):
    1. s:字符串s
    2. p:字符模式p
    3. sL:从字符串s的sL索引处开始匹配
    4. pL:从字符模式p的pL索引处开始匹配
    5. return:如果字符模式 p[pL,... ,N]完全匹配字符串s[sL,... ,M],则返回true,否则返回false
    6. process递归函数的解释:判断字符模式p从索引pL开始一直到结束的的子串是否完全匹配字符串s从索引sL开始到结束的子串,如果完全匹配,返回true,否则,返回false
  3. 递归的终止条件:对于递归函数来说首先需要找到递归的结束条件
    1. 如果sL==s.length:这种情况下,字符串s已经完全被匹配完了(s的最后一个字符的索引为s.length-1),但是字符模式p可能还有字符没有用完,需要分情况讨论
      1. pL==p.length:字符模式p的所有字符都用完了(字符模式p的最后一个字符的索引为p.length-1),显然这种情况下,字符模式p完全匹配字符串s,递归函数返回true
      2. 否则:字符模式p还没有用完,但是这种情况下并不意味着正则表达式没有完全匹配字符串s,因为如果后面的字符全是*,可以让其匹配空字符消耗掉,这种情况下,也是完全匹配的,递归函数返回true,否则返回false
    2. 否则,即sL!=s.length,pL==p.length:这种情况下,字符串s还没有匹配完,但是字符模式p消耗完了,这种情况下返回false
  4. 递归的终止条件找到了,接下来就是递归的转移过程了,此时,字符串s和字符模式p都有剩余字符,需要分三种情况,对于字符模式p,只有三种字符"?","*","a-z"
    1. 如果p[pL]=='?':表示当前?可以匹配任意一个字符,一定能够匹配成功,所有直接return process(s,p,sL+1,pL+1)
    2. 如果p[pL]=='*':可以选择,让当前的*匹配0个字符,1个字符,多个字符
      1. 匹配0个字符:p1=process(s,p,sL,pL+1)。会消耗掉一个*
      2. 匹配1个字符:p2=process(s,p,sL+1,pL+1),会消耗掉一个*
      3. 匹配多个字符:p2=process(s,p,sL+1,pL),这时,不消耗掉*,让其继续匹配
      4. 如果p1,p2,p3任意一个为true,则字符模式p是完全匹配字符串s的
      5. 最终返回p1||p2||p3
    3. 否则,即p[pL]是普通的小写字符:
      1. 如果p[pL]==s[sL]:return process(s,p,sL+1,pL+1)
      2. 否则:return false

超时代码:

class Solution {
   public static boolean isMatch(String s, String p) {
        return process(s.toCharArray(),p.toCharArray(),0,0);
    }
    public static boolean process(char[] s,char[] p,int sL,int pL){
        if (sL==s.length){
            if (pL==p.length){
                return true;
            }else {
                while (pL<p.length){
                    if (p[pL]=='*'){
                        pL++;
                    }else {
                        break;
                    }
                }
                return pL>=p.length;
            }
        }

        if (pL==p.length){
            return false;
        }

        if (p[pL]=='?'){
            return process(s,p,sL+1,pL+1);
        }else if (p[pL]=='*'){
            //匹配0个
            boolean p1 = process(s,p,sL,pL+1);
            //匹配一个
            boolean p2 = process(s,p,sL+1,pL+1);
            //匹配多个
            boolean p3 = process(s,p,sL+1,pL);
            return p1||p2||p3;
        }else {
            if (p[pL]==s[sL]){
                return process(s,p,sL+1,pL+1);
            }else {
                return false;
            }
        }
    }
}

 时间超了,暴力递归的时间复杂度太高,有很多重复的递归。不过只要不是答案错误,说明算法整体思路是没有问题的,只是时间复杂度太高。。。。

注意:只要写出了递归解法,就可以根据递归直接修改为动态规划解法,步骤如下:

  1. dp数组的维度与大小:递归函数boolean process(char[] s,char[] p,int sL,int pL)中的一直在变化的参数只有两个sL和pL,所以只需要两个维度就可以表示所有的递归过程。右因为递归的终止条件为sL==s.length或者pL==p.length,即从0到s.length,0到p.length,所以使用dp[s.length+1][p.length+1]就可以保存所有的递归函数。其中dp[sL][pL]=process(char[] s,char[] p,int sL,int pL),dp[i][j]表示字符模式p从索引j开始一直到结束的的子串是否完全匹配字符串s从索引i开始到结束的子串,如果完全匹配,返回true,否则,返回false
  2. dp数组的初始化值:递归方程的终止条件就是dp数组的初始值
    1. 递归方程的终止条件为
      if (sL==s.length){
          if (pL==p.length){
              return true;
          }else {
              while (pL<p.length){
                  if (p[pL]=='*'){
                      pL++;
                  }else {
                      break;
                  }
              }
              return pL>=p.length;
          }
      }
      
      if (pL==p.length){
          return false;
      }
    2. 所以dp数组的初始值为
      boolean [][] dp = new boolean[s.length+1][p.length+1];
      dp[s.length][p.length] = true;
      
      int i = p.length-1;
      //字符串s到达结尾
      while (i>=0){
          if (p[i]=='*'){
              //虽然p还没有被消耗完,但是如果还没有被消耗的字符是*,这种情况也是完全匹配的
              dp[s.length][i]=true;
              i--;
          }else {
              break;
          }
      }
      //字符串s到达结尾,p还没有到达结尾,并且,还没有被消耗的字符不是*,显然是不完全匹配的,返回false
      while (i>=0){
          dp[s.length][i]=false;
          i--;
      }
      
      //字符模式p到达结尾,字符串s还没有匹配完,返回false
      for (i=0;i<s.length;i++){
          dp[i][p.length]=false;
      }

      上述初始化后,可以得到如下的dp初始值

      绿色表示dp数组中已经初始化的元素

  3. dp的状态转移:

    在递归求解中,发现对于递归process(s,p,sL,pL)函数,在本次递归中可能会进入的递归有:process(s,p,sL+1,pL+1),process(s,p,sl,pL+1),process(s,p,sL+1,pL)。由dp[sL][pL]=process(char[] s,char[] p,int sL,int pL)得出,dp[sL][pL]得值可能与dp[sL+1][pL+1],dp[sl][pL+1],dp[sL+1][pL]有关如下图所示

     所以dp数组得更新方向也就确定,从右往左,从下往上(根据已知得值求解未知得值)。剩下得就是将出现return process()函数得地方修改为dp[sL][pL]=dp[][]就可以了

  4. AC代码

class Solution {
   public static boolean isMatch(String s, String p) {
        return dp(s.toCharArray(), p.toCharArray());
    }

    public static boolean dp(char[] s, char[] p) {
        boolean[][] dp = new boolean[s.length + 1][p.length + 1];
        dp[s.length][p.length] = true;

        int i = p.length - 1;
        //字符串s到达结尾
        while (i >= 0) {
            if (p[i] == '*') {
                //虽然p还没有被消耗完,但是如果还没有被消耗的字符是*,这种情况也是完全匹配的
                dp[s.length][i] = true;
                i--;
            } else {
                break;
            }
        }
        //字符串s到达结尾,p还没有到达结尾,并且,还没有被消耗的字符不是*,显然是不完全匹配的,返回false
        while (i >= 0) {
            dp[s.length][i] = false;
            i--;
        }

        //字符模式p到达结尾,字符串s还没有匹配完,返回false
        for (i = 0; i < s.length; i++) {
            dp[i][p.length] = false;
        }

        for (int sL = s.length - 1; sL >= 0; sL--) {
            for (int pL = p.length - 1; pL >= 0; pL--) {
                if (p[pL] == '?') {
                    dp[sL][pL] = dp[sL + 1][pL + 1];
                } else if (p[pL] == '*') {
                    dp[sL][pL] = dp[sL][pL + 1] || dp[sL + 1][pL + 1] || dp[sL + 1][pL];
                } else {
                    if (p[pL] == s[sL]) {
                        dp[sL][pL] = dp[sL + 1][pL + 1];
                    } else {
                        dp[sL][pL] = false;
                    }
                }
            }
        }
        return dp[0][0];
    }
}

 

从暴力递归到动态规划得过程中,完全没有求解dp状态转移方程,而是根据递归函数到dp的转换,这种方式对于求解不出dp状态转移方程的情况下非常有用!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/729638.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机网络?

那么这样能通过审核吗&#xff1f;

二次元古代美女【InsCode Stable Diffusion美图活动一期】

二次元古代美女【InsCode Stable Diffusion美图活动一期】 一、前言二、初识 InsCode三、 试玩 Stable Diffusion 模型1.阅读Stable Diffusion 模型在线引导说明2.实际体验 Stable Diffusion 模型 四、模型相关版本和参数配置&#xff1a;五、图片生成提示词与反向提示词六、种…

游戏术语英语

王者荣耀英文术语大全&#xff01;玩这么久你都听懂了吗&#xff1f; 王者荣耀AP、AD、ADC、AOE等专业术语大全_乐游网 Operating System win2003, winXP, win7, win10 MacOS Game Platform 游戏平台 TGP&#xff08;Tencent Game Platform &#xff09; PC &#xff08;Per…

Linux上部署docker与docker-compose的步骤

Centos上部署docker与docker-compose的步骤 linux系统版本为Centos7.2 第一步-检查前置条件是否符合部署docker 64-bit 系统 kernel 3.10 使用uname -r 检查内核版本&#xff0c;返回的值大于3.10即可。 Centos 7.2的kernel是&#xff1a;3.10.0-327&#xff0c;刚好满足条件…

【算法与数据结构】225、LeetCode用队列实现栈

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;第一种解法是利用两个队列&#xff0c;一个用作输出队列&#xff0c;一个用作备份队列。主要难点在于p…

<Java导出Excel> 4.0 Java实现Excel动态模板字段增删改查

思路&#xff1a; 主要是同时操作两张表&#xff1a;一张存储数据的表&#xff0c;一张存储模板字段的表&#xff1b; 查询&#xff1a;只查询模板字段的表&#xff1b; 新增&#xff0c;修改&#xff0c;删除&#xff1a;需要同时操作两张表中的字段 如果两张表字段不一致&…

51单片机--点亮LED灯和流水灯

文章目录 前言LED模块的原理点亮一个LED灯LED灯的闪烁LED流水灯 前言 大家好&#xff0c;这里是诡异森林。我使用的是普中科技的A2的51开发板&#xff0c;适合新手入门。用到的应用是Keil5和Stc-isp&#xff0c;第一个软件主要用来写代码的&#xff0c;第二个是将代码程序输送…

RocketMQ5.0--部署与实例

RocketMQ5.0–部署与实例 一、Idea调试 1.相关配置文件 在E:\rocketmq创建conf、logs、store三个文件夹。从RocketMQ distribution部署目录中将broker.conf、logback_namesrv.xml、logback_broker.xml文件复制到conf目录。如下图所示。 其中logback_namesrv.xml、logback_b…

2.2.cuda驱动API-初始化和检查的理解,CUDA错误检查习惯

目录 前言1. cuInit-驱动初始化2. 返回值检查总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记 本次课程学习精简 CUDA 教程-Driver API 案例…

氢燃料电池汽车储氢技术及其发展现状

摘要&#xff1a; 氢能的发展可有效地解决经济发展和生态环境间日益增长的矛盾。氢燃料汽车将处于氢能产业体系中核心地位&#xff0c;加快对氢燃料电池车的技术研发&#xff0c;大范围提高氢能源利用率&#xff0c;对于全世界形成以低碳排放为特征的工业体系具有重要意义。在…

【数据库】忘记mysql本地密码

目录 说明 操作步骤操作失败解决1.在以上操作步骤的第四步&#xff0c;输入mysql&#xff0c;报错第一种报错解决办法如下 第二种报错解决办法如下 2.从上面操作第二步后重新操作步骤如下报错解决办法如下 参考链接 说明 太久没使用本地mysql数据库&#xff0c;忘记了密码。 …

禅意工作-诗意生活

“禅意工作&#xff0c;诗意生活”能做到这两点&#xff0c;非常非常非常难。 AI的解释&#xff1a; “禅意工作&#xff0c;诗意生活”是一种追求内心平和与幸福的生活方式&#xff0c;它将工作与生活相结合&#xff0c;达到一种和谐的状态。以下是一些关于如何实现“禅意工…

GitHub快速上手--GitHub高效操作教程

一、前言 如果你正在看我的这篇文章&#xff0c;说明你已经对GitHub有了一些基础的了解&#xff0c;下面我们将详细叙述每一步的操作&#xff0c;以保证你能够快速上手GitHub&#xff0c;完成对代码的管理。 二、创建仓库 登录GitHub账号&#xff0c;点击页面右上角的加号&am…

flutter聊天界面-自定义表情键盘实现

flutter聊天界面-自定义表情键盘实现 flutter 是 Google推出并开源的移动应用开发框架&#xff0c;主打跨平台、高保真、高性能。开发者可以通过 Dart语言开发 App&#xff0c;一套代码同时运行在 iOS 和 Android平台。 flutter开发基础腾讯IM的聊天应用&#xff0c;使用的是t…

PADS Layout中显示与布线标签页参数设置

1.“显示”标签页如图1 所示&#xff1a; 图1 显示标签页 显示标签页是用于去设置网络名以及管脚编号的字体大小的设置&#xff0c;建议是可以采取默认设置的&#xff0c;如果自己设计有另外要求&#xff0c;也是可以去进行设置。 2.“布线”标签也有三个子标签&#xff0c;首先…

基于matlab使用两个图像估计校准相机的姿势(附源码)

一、前言 运动结构 &#xff08;SfM&#xff09; 是从一组 3-D 图像估计场景的 2-D 结构的过程。此示例演示如何从两个图像估计校准相机的姿势&#xff0c;将场景的三维结构重建为未知比例因子&#xff0c;然后通过检测已知大小的对象来恢复实际比例因子。 此示例演示如何从使…

2.标识符、关键字、保留字

1、标识符 标识符&#xff1a;就是指开发人员为变量、属性、函数、参数取的名字 注意&#xff1a;标识符不能是关键字或保留字 JavaScript标识符 在JavaScript中&#xff0c;标识符&#xff08;Identifier&#xff09;是用于标识变量、函数、对象、属性或其他编程元素的名称。…

如何实现CesiumJS的视效升级?

CesiumJS作为一款强大的地理可视化引擎&#xff0c;为我们提供了丰富的地球数据可视化和交互展示的能力。然而&#xff0c;随着用户需求的不断增加和技术的不断进步&#xff0c;如何进一步提升CesiumJS的视觉效果成为了一个重要的问题。 首先&#xff0c;为了实现CesiumJS视觉…

Docker(二)之容器技术所涉及Linux内核关键技术

容器技术所涉及Linux内核关键技术 一、容器技术前世今生 1.1 1979年 — chroot 容器技术的概念可以追溯到1979年的UNIX chroot。它是一套“UNIX操作系统”系统&#xff0c;旨在将其root目录及其它子目录变更至文件系统内的新位置&#xff0c;且只接受特定进程的访问。这项功…

国内几款常用热门音频功放芯片-低功耗、高保真

音频功放芯片&#xff0c;又称为音频功率放大器芯片&#xff0c;是指一种将音频信号转换成线性的输出功率的集成电路芯片&#xff0c;在音频功放领域中一类是传统意义上的模拟功放&#xff1b;另一类是数字功放&#xff0c;它们都可以实现模拟信号到数字信号的转换。 随着智能…