模式串匹配方式:
在模式串与字符串的匹配中,我们处理匹配这类的问题一般都是动态规划。但是,我不知道读者是否知道为什么选用动态规划?这要从动态规划的特性说起来。
动态规划:动态规划是记录每一个可达状态的情况,从而化大为小。同时,可达状态也表示
了状态是可转移的,可记录(可被设计)的。
回溯:强调每一个状态的可回退性,也就是我们发现情况不对,可以从恰当的“起点”重新来
过。
递归:从严格意义上,递归只是指函数调用函数本身。但是递归确实是实现“回溯算法”的重要
手段。但是可以记忆化搜索的递归,也是一种动态规划,所以动态规划也不应该限制于递
推。
这些概念相当接近,因为它们在大部分情况下是有联系的。所以动态规划不能只是单纯的空间换时间!因为这不是它的特性,空间换时间是大部分优化时间复杂度的算法特性。
而在模式串匹配中,我们通常都想着,如果两个字符串中有两个字母匹对成功,那么加上新字母的子串是否匹配成功,取决于老状态是否匹配。
新状态依赖老状态,或者说新状态可由老状态转移而来。所以我们只需要记录每一个匹配状态就好。
匹配规则1:
'?'
可以匹配任何单个字符。'*'
可以匹配任意字符序列(包括空字符序列)。
(声明:题目取自leetcode)
现在我们就知道我们需要去记录一个匹配状态。
所以我们可以设计一个用记录状态的数组dp(可称记忆数组),含义为s[0,i-1]于p[0,j-1]的匹配情况;
int m = s.size();
int n = p.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
接下来我们先考虑状态的转移:
1.如果是p[j] == '?' || p[j] == s[i] 那么 dp[i][j] = dp[i-1][j-1];
解释:也就取决于加入匹配字符前的状态
2.如果 p[j] == '*' 那么 dp[i][j] = dp[i][j-1] | dp[i-1][j];
解释:如果是‘*’,那么有两种情况。(1)‘*’匹配空; (2)‘*’匹配1个或多个字符;
当匹配空时,也就是说明看 dp[i][j-1]的记录;如果非空,就看dp[i-1][j];
但是不容易理解的是为什么是dp[i-1][j],而不是dp[i-1][j-1];
答案在于多个匹配。从含义上说[j-1]将‘*’剔除,也就是说‘*’只匹配一次。而[j]是包含‘*’意味着可以 匹配多次。但是从表达式上说,匹配一个是dp[i-1][j-1] 两个是看dp[i-2][j-1] 匹配k个 dp[i-k][j-1];
我们会发现匹配发生在子串末尾,并且每次匹配都是短子串先行!所以当我们记录新子串匹配情 况时,dp[i][j] = dp[i-1][j]; dp[i-1][j] = dp[i-2][j];...;dp[i-k+1][j] = dp[i-k][j];因为‘*’匹配k个字符。所以 dp[i-k][j]是空匹配的状态也就是 dp[i-k][j] = dp[i-k][j-1];
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (p[j - 1] == '*') {
dp[i][j] = dp[i][j - 1] | dp[i - 1][j];
}
else if (p[j - 1] == '?' || s[i - 1] == p[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
接下来是初始状态设计,我们知道。空字串只能跟空子串匹配,或者跟‘*’匹配。但是这个‘*’只能是开头。否则就不是从头至尾的匹配,而是可以从中间匹配;具体原因就是dp[0]的作用不仅仅是初始化,还会在匹配中起到切割作用。例如dp[0][j] = 1;意味着,s可以从p模式串的第j位开始匹配,最本质的意思是现在p[0-j]视为空集合。
for (int i = 1; i <= n; ++i) {
if (p[i - 1] == '*') {
dp[0][i] = true;
}
else {
break;
}
}
class Solution {
public:
bool isMatch(string s, string p) {
int m = s.size();
int n = p.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
dp[0][0] = true;
for (int i = 1; i <= n; ++i) {
if (p[i - 1] == '*') {
dp[0][i] = true;
}
else {
break;
}
}
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (p[j - 1] == '*') {
dp[i][j] = dp[i][j - 1] | dp[i - 1][j];
}
else if (p[j - 1] == '?' || s[i - 1] == p[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
return dp[m][n];
}
};
如果去掉初始化的退出部分可以试试以下测试样例:
s = “ab” p = "a*ab";
匹配规则2:
(声明:题目取自leetcode)
这个道题跟之前一样,但是不同的是,现在‘*’只能匹配多个一种字符。也就是所会有一种字符跟“*”捆绑成一体。
class Solution {
public:
bool isMatch(string s, string p) {
int m = s.size();
int n = p.size();
vector<vector<bool> > f(m+1, vector<bool>(n+1, false));
f[0][0] = true;
//i = 0 时初始化也是从前往后初始化防止从中间开始匹配 但是允许跳过星号,注意题目意思“c*”
//是捆绑的整体,即该*只匹配任意个c字符可以为0。
for(int i = 0; i <= m; ++i)
{
for(int j = 1; j <= n; ++j)
{
if(p[j-1] == '*')
{
f[i][j] = f[i][j] | f[i][j-2];
if(i == 0) continue;
if(p[j-2] == '.' || p[j-2] == s[i-1])
{
f[i][j] = f[i][j] | f[i-1][j];
}
}
else
{
if(i == 0) continue;
if(p[j-1] == '.' || p[j-1] == s[i-1])
{
f[i][j] = f[i][j] | f[i-1][j-1];
}
}
}
}
return f[m][n];
}
};