题目
暴力递归
首先,我们先从简单的暴力递归开始解题。
1. 边界条件
递归的边界条件非常好得到,那就是 两个字符串为空时,递归就应该结束了:
- 如果两个字符串此时都空了,那肯定返回 true,因为 空 匹配 空 肯定是 true。返回 true;
- 如果 p 空了,而 s 没空,那此时肯定说明匹配不成功,显然 s 还有一段没匹配到。返回 false;
- 如果 s 空了,而 p 没空,这个时候其实还没办法判断。等后面处理
这里为什么说 s 为空时没办法判断呢?主要是这个题的测试样例里面有这种情况:
s = "aa", p = "a*b*"
,此时结果返回 true。显然这和我们平常理解的得先有个 b 不同,*号意味着前面这个 b 也可有可无,所以我们这里没办法直接通过 s 为空来判断 返回 false,因为 p 剩下的部分可能是"a*b*c*"
这种字符串,这一大串匹配 空 时是 true。
2. 操作块
有了边界条件,我们就需要思考递归的操作是怎样处理这两个字符串的。一共有几种情况:
- 如果说 s 空了,而 p 没空(最外层的 else 部分):
此时我们开始对 ‘*’ 进行判断。- 如果
p[1] == '*'
即(s == "", p == "a*b*c*")
: 那说明 p[0] 这个字母是可有可无的。那我只需要接着判断 s 和 p.substr(2) 就行了。一个例子就是s="", p="a*b*c*"
的比较结果和s="", p="b*c*"
是相同的。 - 如果
p[1] == nullptr || p[1] != '*'
即(s == "", p == "abc" 或者 p == "a")
: 此时就是说明,p 不可继续消除了,那肯定是 false 了。
- 如果
- 如果说 s, p 都不为空,此时
p[0]
和s[0]
是相等的,即p[0] == '.' || p[0] == s[0]
我们仍然对 ‘*’ 进行判断- 如果
p[1] == '*'
: 在这种情况下,我们实际上有多种选择p == "a*b"
来当作p == ""
来使用:此时,我们将 “a*” 当作 0 个 a 来看待,这和上面情况是一致的。那我们只需要比较 s 和 p.substr(2) 即可,即s == "ab", p == "a*ab"
的比较结果和s == "ab", p == "ab"
的比较结果相同。p == "a*b"
来当作p == "ab"
来使用: 此时,我们将 “a*” 当作 1 个 a 来看待。那我们只需要比较 s.substr(1) 和 p.substr(2) 即可,即s == "ab", p == "a*ab"
的比较结果和s == "b", p == "ab"
的比较结果相同。p == "a*b"
来当作p == "aa*b"
来使用: 此时,我们将 “a*” 当作 1 个以上 a 来看待。那我们只需要比较 s.substr(1) 和 p 即可,即s == "ab", p == "a*ab"
的比较结果和s == "b", p == "a*ab"
的比较结果相同。
- 如果
p[1] != '*'
: 这种情况就非常好判断了,直接返回 s.substr(1) 和 p.substr(1) 的匹配结果就行了,即s == "ab", p == "ac"
的匹配结果与s == "b", p == "c"
的匹配结果相同。
- 如果
- s,p 都不为空,且p[0] s[0] 也不相等
有了这些结果,我们就可以撰代码了。
代码
class Solution {
public:
bool isMatch(string s, string p) {
return helper(s, 0, p, 0);
}
bool helper(string& s, int sbeg, string& p, int pbeg) {
if (sbeg == s.size() && pbeg == p.size()) return true;
else if (pbeg == p.size()) return false;
if (sbeg != s.size() && (p[pbeg] == '.' || p[pbeg] == s[sbeg])) {
if (pbeg < p.size() - 1 && p[pbeg + 1] == '*')
return helper(s, sbeg, p, pbeg + 2) ||
helper(s, sbeg + 1, p, pbeg + 2) ||
helper(s, sbeg + 1, p, pbeg);
else
return helper(s, sbeg + 1, p, pbeg + 1);
} else {
if (pbeg < p.size() - 1 && p[pbeg + 1] == '*')
return helper(s, sbeg, p, pbeg + 2);
}
return false;
}
};
这里因为需要获取子串,为了减少开销,我这里使用的 index 来替代获取子串的操作。这个代码无疑是超时的。
记忆化搜索
有了暴力递归,虽然超时,但是我们仍然可以将他朝着动态规划的方向进行优化。其实我们在上述代码中很容易就能发现递归的问题:对于同一个问题求解了多次。
你可以看到上面有很多 sbeg + 1, pbeg + 2 这种运算,这些值算出来很有可能相等,那么对于同一个 helper(s, 4, p, 5) 这种函数,可能会调用了多次。那如何来减少调用呢?可以选择使用一个全局的 map 来存储这些内容。每当第一次调用时就填充 map 后续如果还需要这个值就直接返回。
这里我的 map 使用 vector 来代替,大小是 (s.size() + 1) * (p.size() + 1) 的大小,每次取值时,通过 getIndex 来找出 vector 中对应的位置下标。
代码
class Solution {
public:
bool isMatch(string s, string p) {
row = s.size() + 1;
col = p.size() + 1;
v_map = vector<int>(row * col);
return helper(s, 0, p, 0);
}
bool helper(string& s, int sbeg, string& p, int pbeg) {
if (sbeg == s.size() && pbeg == p.size()) return true;
else if (pbeg == p.size()) return false;
if (sbeg != s.size() && (p[pbeg] == '.' || p[pbeg] == s[sbeg])) {
if (pbeg < p.size() - 1 && p[pbeg + 1] == '*')
{
if (v_map[getIndex(sbeg + 1, pbeg + 2)] == 0) v_map[getIndex(sbeg + 1, pbeg + 2)] = helper(s, sbeg + 1, p, pbeg + 2) ? 1 : -1;
if (v_map[getIndex(sbeg + 1, pbeg + 2)] == 1) return true;
if (v_map[getIndex(sbeg, pbeg + 2)] == 0) v_map[getIndex(sbeg, pbeg + 2)] = helper(s, sbeg, p, pbeg + 2) ? 1 : -1;
if (v_map[getIndex(sbeg, pbeg + 2)] == 1) return true;
if (v_map[getIndex(sbeg + 1, pbeg)] == 0) v_map[getIndex(sbeg + 1, pbeg)] = helper(s, sbeg + 1, p, pbeg) ? 1 : -1;
if (v_map[getIndex(sbeg + 1, pbeg)] == 1) return true;
return false;
}
else {
if (v_map[getIndex(sbeg + 1, pbeg + 1)] == 0) v_map[getIndex(sbeg + 1, pbeg + 1)] = helper(s, sbeg + 1, p, pbeg + 1) ? 1 : -1;
if (v_map[getIndex(sbeg + 1, pbeg + 1)] == 1) return true;
}
} else if (pbeg < p.size() - 1 && p[pbeg + 1] == '*') {
if (v_map[getIndex(sbeg, pbeg + 2)] == 0) v_map[getIndex(sbeg, pbeg + 2)] = helper(s, sbeg, p, pbeg + 2) ? 1 : -1;
if (v_map[getIndex(sbeg, pbeg + 2)] == 1) return true;
}
return false;
}
private:
vector<int> v_map;
int row;
int col;
int getIndex(int a, int b) {
return a * col + b;
}
};
动态规划
最后,我们根据记忆化搜索的代码无脑转换成动态规划。
明确含义
首先,我们先明确 dp 的含义, dp[s.size() + 1][p.size() + 1]
。 dp[i][j] 代表着 s.substr(i, end) 可以通过 p.substr(j, end) 来匹配 是 true 还是 false。
初始化 dp
然后我们根据递归的终止条件初始化 dp:
- 如果两个字符串此时都空了,返回 true ==> dp[s.size()][p.size()] = true;
- 如果 p 空了,而 s 没空, 返回 false ==> dp[*][p.size()] = false; 这一步不用做,因为 vector 默认初始化为 false
开始循环
然后我们开始写循环:
for (int i = s.size(); i > -1; --i) {
for (int j = p.size() - 1; j > -1; --j)
逆着遍历,因为你看到了,我们的终止条件在 dp[s.size()][p.size()] 处。
下面,我们需要为每一次循环来做点什么呢?仍然是照着递归来写。
首先,我们先判断 '*'
。你会看到,如果说 p[j] == '*'
,此时我们是不需要这个 dp[i][j] 的值的。因为在递归中,我们处理 '*'
的方式是,要么取 '*'
前面的字符进行操作,要么是取 '*'
后面的字符进行操作,其实也就是 记忆化搜索里的 pbeg 或者 pbeg + 2。所以,我们在 dp 时,从当前视角来看我们不必处理 dp[i][j] 如果 p[j] == '*'
。 continue 掉。
如果说 s 空了,而 p 没空:
- p[j + 1] == ‘': if (v_map[getIndex(sbeg, pbeg + 2)] == 1) return true;
==> dp[i][j] = dp[i][j + 2];
- p[j + 1] != '’: dp[i][j] = false;
不用操作
如果说 s, p 都不为空:
- p[j + 1] == ‘':
if (v_map[getIndex(sbeg + 1, pbeg + 2)] == 1) return true;
+
if (v_map[getIndex(sbeg, pbeg + 2)] == 1) return true;
+
if (v_map[getIndex(sbeg + 1, pbeg)] == 1) return true;
==> dp[i][j] = dp[i][j + 2] || dp[i + 1][j + 2] || dp[i + 1][j];
- p[j + 1] != '’: if (v_map[getIndex(sbeg + 1, pbeg + 1)] == 1) return true;
==> dp[i][j] = dp[i + 1][j + 1];
结果
结果就是 dp[0][0],直接返回就行了。
这样,我们就得到了完整的动态规划代码。
代码
class Solution {
public:
bool isMatch(string s, string p) {
vector<vector<bool>> dp(s.size() + 1, vector<bool>(p.size() + 1)); // dp[i][j] s.substr(i, end) 可以通过 p.substr(j, end) 来匹配
dp[s.size()][p.size()] = true;
for (int i = s.size(); i > -1; --i) {
for (int j = p.size() - 1; j > -1; --j) {
if (p[j] == '*') continue;
if (i == s.size() || (p[j] != '.' && p[j] != s[i])) {
if (j < p.size() - 1 && p[j + 1] == '*')
dp[i][j] = dp[i][j + 2];
} else {
if (j < p.size() - 1 && p[j + 1] == '*')
dp[i][j] = dp[i][j + 2] || dp[i + 1][j + 2] || dp[i + 1][j];
else
dp[i][j] = dp[i + 1][j + 1];
}
}
}
return dp[0][0];
}
};