通配符匹配—动态规划
今天要记的题目是关于动态规划的两道题,没错,两道题,因为很类似,所以就放一起记了
相信很多人应该都有类似的感觉(卧槽,原来是这样的嘛?这递推公式也不怎么难嘛—做题前;额,,额,这递推公式咋想啊~—做题时)。动态规划,主要的在于当你拿到一道题时,要能想到使用动态规划(很多情况,我都是直接硬怼,递归走起,结果不是超时就是错误),其次是能够将问题的细节想明白,将流程中的一小步拿出来单独去看,最后,将这一小步写成递推公式
第一道题
题目介绍
给定一个字符串 (s
) 和一个字符模式 (p
) ,实现一个支持 '?'
和 '*'
的通配符匹配。
1 | '?' 可以匹配任何单个字符。 |
两个字符串完全匹配才算匹配成功。
说明:
s
可能为空,且只包含从a-z
的小写字母。p
可能为空,且只包含从a-z
的小写字母,以及字符?
和*
。
1 | 输入: |
1 | 输入: |
思路
首先看题,无非是字符串之间的匹配,只是加入了两种符号而已~
注意:为解释方便,都默认目前模式串的匹配位置为p[j],即第j+1个字符(从1开始),主串的匹配位置为s[i],在这两个位置前的均已匹配成功
最直接的想法:分情况讨论(p[j]的分类)
- p[j]为a-z间的字符的时候,直接匹配即可,如果p[j] != s[i],直接false,反之j++,i++
- p[j]为’?’符号的时候,表示可以匹配任何单个字符,因此,不用看,直接i++,j++
- p[j]为’*’符号的时候,表示可以匹配任何字符串(注意:这里是与下一道题不同的地方),到这里,就属于比较麻烦的情况了,因为我们不知道这个符号代替的到底是什么?空串?单字符?还是多字符?都有可能,因此继续分情况讨论
- 如果是空串,就看p[j+1]与s[i]的关系
- 如果是单字符呢,就直接看p[j+1]与s[i+1]的关系
- 最麻烦的是多字符,这个时候,仔细想下,多字符?也就是说除了当前s[i]这个字符外,p[j]还会匹配s[i]后面的字符,所以就继续看p[j]与s[i+1]的关系
这就是最直接的思路,也就是递归的思路,不过这个思路中,存在大量的重复计算,导致无法AC,实际上,加上一个用来记忆判断过的下标组合就可以AC,这里就不贴代码了,因为这不是我想要记录的部分(>^ω^<)
递归加记忆化可以通过的题,动态规划怎么可能不行呢?肯定是可以的,常规操作,构建dp数组,dp[i] [j]表示的是指p的前j个字符与s的前i个字符的匹配与否(注意:dp[2] [3]意思是p[2]与s[1]前面的匹配情况,包括p[2]和s[1])
数组定义好后,需要将一些边界关系设置好,比如,dp[0] [0] = true,dp[?] [0] = false (? > 0),dp[0] [?]则需要人为判断下,因为只有当p的前?个字符都是’*’才可以为true
边界关系设置好后,就是递推公式的推导,这也是最烦的地方,一步步来看:
- 当p[j]为a-z字符时,dp[i+1] [j+1] = dp[i] [j] && (p[j] == s[i]) (这很容易懂吧~,就是说,是字符的话,就看p[j]和s[i]前面的匹配与否与p[j],s[i]是否相等共同决定)
- 当p[j]为’?’时,dp[i+1] [j+1] = dp[i] [j]
- 当p[j]为’*’时,也是比较麻烦,分情况讨论:
- ‘*’表示多字符时,dp[i+1] [j+1] = dp[i] [j+1]. 可以看出来,这里的想法和递归刚好是相反的,递归的时候,多字符我们是向后看的,但是这里,我们是往前看的,也就是说这个多字符包含的是前面的字符加上当前字符
- ‘*’表示单字符时,dp[i+1] [j+1] = dp[i] [j].
- ‘*’表示空串时,dp[i+1] [j+1] = dp[i+1] [j]
实际上,对’*’的分类可以更简单点,两种:使用与未使用,使用了的话,dp[i+1] [j+1] = dp[i] [j+1];没使用的话,dp[i+1] [j+1] = dp[i+1] [j]
可能到这,还是有点懵,没事,再简单点来说,首先明确一点,动态规划是根据前面的状态来考虑后面的状态,因此,不用去想p[j]后面的匹配情况。假设我们使用了*号,那么我们是不是无法确定目前的s[i]是代表的多字符的最后一个?因此,需要查看s[i-1]的情况,也就是dp数组中的dp[i] [j+1]
代码
1 | public boolean isMatch(String s, String p){ |
第二道题
题目介绍
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
'.'
匹配任意单个字符'*'
匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s
的,而不是部分字符串。
1 | 输入:s = "aa" p = "a" |
1 | 输入:s = "aa" p = "a*" |
1 | 输入:s = "ab" p = ".*" |
老实说,就是第三个示例让我很迷糊~导致这道题我一直没做,直到把上面那道题做完了后才回去看的这道题
思路
这道题肯定也是有很多思路的啦,比如递归啥的,,只是这里就只讲下动态规划的思路,便于我自己记忆(没错!!我自己~)
与上面那道题的不同在于,这里的’*’号不是可以代替任意字符串的,它必须有个前缀字符,并且其作用是将前缀字符重复任意次,简单点来说,a *就是指可以表示空串,aa,aaa等
那么,这会造成什么不一样的呢?其实区别也不是太大,往下看就知道了~
首先构建dp数组,定义与上面那道题一样
接着,边界有点变化,dp[0] [?]的判断不太一样,这里,如果希望是true,那么p必须类似于a * a* a*这种,而且?必须是 *号下标才可以,那这该怎么弄呢?别忘了,我们这是动态规划,是有前面的状态的,没错,dp[0] [i] 与dp[0] [i-2]是有关系的(当然,仅在p[i-1]为 * 号的时候)
最后就是递推公式,大体上与上道题类似,只是些许不同
- 当p[j] 为a-z字符时,dp[i+1] [j+1] = dp[i] [j] && (p[j] == s[i])
- 当p[j] 为 ‘.’时,dp[i+1] [j+1] = dp[i] [j]
- 当p[j] 为 ‘*’时,分类讨论:
- 使用了 *号:dp[i+1] [j+1] = dp[i] [j+1]. (理由与上道题是相同的,当前s中的字符不一定是 *号代表的最后一个字符,我们不向后看)
- 没使用 *号:dp[i+1] [j+1] = dp[i+1] [j-1] 注意
标注的注意的地方,也就是与上道题不同所在,因为这里的 *号始终是与前面那个字符是绑定的,因此不用的话前面那个字符也被删掉,所以减了2
代码
1 | public boolean isMatch(String s, String p){ |
结束
到这里,关于这两道动态规划的题就结束了,不过很大可能,下次遇到,我还是,,呵呵,你懂的
至于番剧嘛,在追了在追了,对了,不得不吹下斗破的三年之约是真的好看(可能有人会说,好看,我看你只是馋女王的身子~)么的错!!好看的都是我老婆~
我就问,这腰谁不喜欢~~~