文章目录
- 题目描述
- 结果
- 记忆化搜索结果
- 动态规划结果
- 我的记忆化搜索代码
- 网络的题目解析
- 解法一:递归暴力求解
- 解法二:记忆化搜索
- 解法三:动态规划
- 反思
- 参考资料
我是看了很多个测试用例才过的题,以后要改改,不能看测试用例了,机试的时候才不会告诉你测试用例呢!需要警醒!!!
//我的想法是记忆化搜索
//首先是DFS,然后DFS里面引入记忆化的备忘录。。。
//不知道可不可以。。。
//传说记忆化搜索很常见呀
//这里*必须跟在"a-z"或者是"."之后,不会单独出现
class Solution {
private:
//lenS表示s的长度
int lenS;
//lenP表示p的长度
int lenP;
//创建一个dp数组
//dp[i][j]的含义表示s[i]和p[j] 之后 的字符串是否匹配,包含s[i]和p[j]在内。
//初始化0表示未计算,1表示可以,-1表示不可以
vector<vector<int>> dp;
public:
bool isMatch(string s, string p) {
//分别得到两者的长度
lenS = s.size();
lenP = p.size();
//进行一些特殊情况的判断
if((lenS!=0&&lenP==0)){ //正则式为空
return false;
}
if(lenS==0&&lenP==0){ //双方都为空
return true;
}
/*对于s为空的情况,有下面这种特殊情况:
s:""
p:".*"
所以不好直接判断
*/
dp.assign(lenS,vector<int>(lenP,0));
//开始进行我们的dfs求解
int ans = dfs(s,p,0,0);
if(ans==-1){
return false;
}else{
return true;
}
}
/*
dfs的含义是表示s[i]和p[j]之后的字符串是否匹配,包含s[i]和p[j]在内。
ps:position of s
pp:position of p
注意使用字符串时要传地址,这样是为了减少时间的开销,如果赋值会要很大的时间
*/
int dfs(string &s,string &p,int ps,int pp){
//递归边界
if(ps==lenS && pp==lenP){ //如果ps和pp都到达了边界,直接返回1
return 1;
}else if(ps==lenS && pp < lenP){ //如果还有p中还有一些模式没有进行匹配
//特判一下最后一个字符是否是"./(a-z)"是的话,直接返回-1
if((p[lenP-1]>='a'&&p[lenP-1]<='z')||p[lenP-1]=='.'){
return -1;
}else{ //否则,从pp开始排除
for(int i=pp;i<lenP-1;i++){
if((p[i]>='a'&&p[i]<='z'||p[i]=='.')&&p[i+1]!='*'){
return -1;
}
}
return 1;
}
}else if(ps < lenS && pp >=lenP){ //如果s中还剩下字符没有被匹配
return -1;
}
//如果有结果,那么直接返回
if(dp[ps][pp] != 0){
return dp[ps][pp];
}
//否则开始进行递归式
//有很多种情况,首先分为后面有没有*的情况
int ans;
//如果后面带"*",则有不匹配和匹配两种情形了
if(pp < lenP-1 && p[pp+1] == '*'){
int case1=0;
int case2=0;
//如果进行匹配,那么只需要变换s即可
if(s[ps] == p[pp] || p[pp] == '.'){
case1 = dfs(s,p,ps+1,pp);
}else{ //说明匹配失败
case1 = -1;
}
//如果不进行匹配,那么转移到下一位
case2 = dfs(s,p,ps,pp+2);//注意是+2
ans = max(case1,case2);
}else{ //没有"*"的话只能进行匹配
if(s[ps] == p[pp] || p[pp] == '.'){
ans = dfs(s,p,ps+1,pp+1);
}else{
ans = -1;
}
}
dp[ps][pp] = ans;
return dp[ps][pp];
}
};
这道题其实是要实现 Regular Expression 里面的两个符号,一个是 ‘.’,另一个是 ‘*’, 前者表示可以 match 任意一个字符,后者表示其前面的字符可以重复零次或者多次。
题目的难点其实是在于 * 上面,如果没有这个 *,题目会变得非常简单,这里说一下题目的两个隐含条件:
递归方式的暴力深度优先搜索求解方法往往是搜索问题的万金油,这里你只需要简单的考虑两件事情:
- 这个问题是否可以划分为子问题;(即可不可以进行递归调用子问题求解)
- 每个子问题有几种状态,就是在当前考虑的问题下,一共有多少种可能性。(即一个原问题,可以调用几种不同的子问题,通常是if-else语句或者for循环里面是不同的子问题)
知道了这两点后,对于原问题的每个子问题递归求解就行。
(纸上得来终觉浅,绝知此事要躬行,写多了也就体会到了这些啦。)
上面说的可能有点抽象,结合这个题目来做例子,这里的问题是,输入一个字符串 s,以及其匹配字符串 p,要求解这两个字符串是否匹配。
我们首先考虑这个字符串比较的问题能不能划分为一个个的子问题,你发现字符串是可以划分成为一个个字符的,这样字符串比较的问题就会变成字符的比较问题,这样一来,我们就可以把问题看成,决定 s[i,…n] 是否能够匹配 p[j,…m] 的条件是子问题 s[i+1,…n] 能不能够匹配 p[j+1,…m],另外还要看 s[i] 和 p[j] 是否匹配, 但是这里的当前要解决的问题是 s[i] 和 p[j] 是否匹配,只有这一点成立,我们才有继续递归去看 s[i+1,…n] 是匹配 p[j+1,…m]。
注意这里我说 s[i] p[j], 并不表示说当前就只用考虑这两个字符之间匹不匹配,它只是用来表示当前问题,这个当前问题也许只需要比较一个字符,也许要比较多个,这就引申出了前面提到的第二点,我们还需要考虑当前问题中的状态。
对于字符串 s 来说,没有特殊字符,当前问题中字符只会是字母,但是对于 p 来说,我们需要考虑两个特殊符号,还有字母,这里列举所有的可能,如果说当前的子问题是 s[i,…n] 和 p[j…m]:
这里我解释下第三种情况,之前在题目描述里说过,p 的起始字符不可能是 *,也就是说 * 的前面必须有字母,根据定义,这里我们可以把 * 的前面的元素个数算作是零个,这样我们就只用看,s[i,…n] 和 p[j+2,…n] 是否匹配,如果算作一个或者多个,那么我们就可以看 s[i+1,…n] 和 p[j,…m] 是否成立,当然这个的前提是 p[j] == s[i] 或者 p[j] == ‘.’, 我们可以结合代码来看看
class Solution {
public boolean isMatch(String s, String p) {
if (s.equals(p)) {
return true;
}
boolean isFirstMatch = false;
if (!s.isEmpty() && !p.isEmpty() && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.')) {
isFirstMatch = true;
}
if (p.length() >= 2 && p.charAt(1) == '*') {
// 看 s[i,...n] 和 p[j+2,...m] 或者是 s[i+1,...n] 和 p[j,...m]
return isMatch(s, p.substring(2))
|| (isFirstMatch && isMatch(s.substring(1), p));
}
// 看 s[i+1,...n] 和 p[j+1,...m]
return isFirstMatch && isMatch(s.substring(1), p.substring(1));
}
}
上面的实现之所以被称为暴力求解是因为子问题的答案没有被记录,也就是说如果当前要用到之前的子问题的答案,我们还得去计算之前计算过的子问题。
所以我们要使用备忘录,也就是我们的记忆化搜索。
上面的暴力解法是因为没有记录答案,记忆化搜索是在 “傻搜” 的基础之上添加 “记事本”。这里我把递归的方向给改变了,当然这不是必要的,主要想说明,对于递归来说,从后往前考虑和从前往后考虑都是可行的。
我们假设当前问题是考虑 s 的第 i 个字母,p 的第 j 个字母,所以这时的子问题是 s[0…i] 和 p[0…j] 是否匹配:
p[j] 是字母,并且 s[i] == p[j],当前子问题成立与否取决于子问题 s[0…i-1] 和 p[0…j-1] 是否成立
p[j] 是 ‘.’,当前子问题成立与否取决于子问题 s[0…i-1] 和 p[0…j-1] 是否成立
p[j] 是字母,并且 s[i] != p[j],当前子问题不成立
p[j] 是 ‘*’,s[i] == p[j – 1],或者 p[j – 1] == ‘.’, 当前子问题成立与否取决于子问题 s[0…i-1] 和 p[0…j] 是否成立(这里应该还要考虑不进行匹配的情况,比如s为:aa,p为: a * aa)。
p[j] 是 ‘*’,s[i] != p[j – 1],当前子问题正确与否取决于子问题 s[0…i] 是否匹配 p[0,…j-2]。
不管是从前往后,还是从后往前,你可以看到,考虑的点都是一样的,只是这里我们多加了一个 “记事本”
public boolean isMatch(String s, String p) {
if (s.equals(p)) {
return true;
}
boolean[] memo = new boolean[s.length() + 1];
return helper(s.toCharArray(), p.toCharArray(),
s.length() - 1, p.length() - 1, memo);
}
private boolean helper(char[] s, char[] p, int i, int j, boolean[] memo) {
if (memo[i + 1]) {
return true;
}
if (i == -1 && j == -1) {
memo[i + 1] = true;
return true;
}
boolean isFirstMatching = false;
if (i >= 0 && j >= 0 && (s[i] == p[j] || p[j] == '.'
|| (p[j] == '*' && (p[j - 1] == s[i] || p[j - 1] == '.')))) {
isFirstMatching = true;
}
if (j >= 1 && p[j] == '*') {
// 看 s[0,...i] 和 p[0,...j-2]
boolean zero = helper(s, p, i, j - 2, memo);
// 看 s[0,...i-1] 和 p[0,...j]
boolean match = isFirstMatching && helper(s, p, i - 1, j, memo);
if (zero || match) {
memo[i + 1] = true;
}
return memo[i + 1];
}
// 看 s[0,...i-1] 和 p[0,...j-1]
if (isFirstMatching && helper(s, p, i - 1, j - 1, memo)) {
memo[i + 1] = true;
}
return memo[i + 1];
}
有了上面两种方法和解释作为铺垫,我想迭代式的动态规划应该不难理解。这里我们不再用递归,而是使用 for 循环的形式,先上代码:
class Solution{
public:
bool isMatch(string s, string p)
{
int sSize = int(s.size());
int pSize = int(p.size());
if (p.empty()) //很妙
{
return s.empty();
}
//dp大小应该比s,p的size大1
vector<bool> tmpVec(pSize + 1, false);
vector<vector<bool>> dp(sSize + 1, tmpVec); //dp[i][j] 表示 s 的前 i 个是否能被 p 的前 j 个匹配
dp[0][0] = true;
//初始化情况:s为空,p为.*.*的情况
for(int i=1;i<=pSize;++i){
//这里包含了考虑dp[0][1]的情况
dp[0][i] = p[i-1] == '*' ? dp[0][i-2] : false;
}
for (int i = 1; i <= sSize; i++)
{
for (int j = 1; j <= pSize; j++)
{
if (p[j-1] == '.' || p[j-1] == s[i-1]){
//如果是任意元素 或者是对于元素匹配
dp[i][j] = dp[i-1][j-1];
}
if (p[j-1] == '*'){
if (p[j - 2] != s[i-1] && p[j - 2] != '.'){
//不能匹配,那么只能不匹配
dp[i][j] = dp[i][j - 2];
}
else{
//能够匹配也要分两种情况——需要匹配和不需要匹配,两者并即可,不必细思
//因为考虑了ab abb*,所以是不需要匹配的
dp[i][j] = (dp[i-1][j] || dp[i][j - 2]);
}
}
}
}
return dp[sSize][pSize];
}
};
这里我说一下前面的 DP 数组的初始化,因为需要考虑空串的情况,所以我们 DP 数组大小多开了 1 格。dp[0][0] = true 因为两个空串是匹配的,紧接着下面一行的 for 循环是为了确保空串和 p 的一部分是匹配,比如 s = “”,p = “a*b”,那么这里 dp[0][2] = true,也就是 s[0,0]和p[0,2] 是匹配的,注意和之前不一样的是这里的 0 代表空串。