0x00 题目
Given an input string
(s)
and a pattern(p)
, implement wildcard
pattern matching with support for'?'
and'*'
.
'?'
Matches any single character.
'*'
Matches any sequence of characters (including the empty sequence).The matching should cover the entire input string (not partial).
Note:
s
could be empty and contains only lowercase letters a-z.
p
could be empty and contains only lowercase letters a-z, and
characters like?
or*
.Example 1:
Input:
s = "aa"
p = "a"
Output:false
Explanation:"a"
does not match the entire string"aa"
.Example 2:
Input:
s = "aa"
p = "*"
Output:true
Explanation:
'*'
matches any sequence.
题目链接:https://leetcode.com/problems/wildcard-matching/description/
相信大家应该是见过这个题目的,而且用暴力+回溯的方式解这道题目读者应该不在少数。
不过尴尬的是笔者在做完之后才发现暴力+回溯可以更快的得到答案。
0x01 动态规划粗暴版本
笔者用了动态规划的思路,先看下最粗暴的动态规划版本:
记 dp[i][j] 表示 p[0…i] 能否匹配 s[0…j],
初始状态:
- 显然
dp[0][0] = true
,空串匹配空串是可以的。- 同时,
dp[0][1...s.size()] = false
,空串不能匹配任何非空串。
状态转移
对于任意 i (1<= i <= p.size()) 和 j (0 <= j <= s.size())
,考察 ch = p[i-1]
:
- 如果
ch == '*'
,dp[i][j] 为 true
,取决于dp[i-1][0...j]
中有没有true
,意思是用*
表示s[0...j-1]
的任意后缀进行尝试。- 如果
ch == '?'
,dp[i][j] 为 true
,取决于dp[i-1][j-1]
是否为true
。- 否则,
dp[i][j] 为true
,取决于dp[i-1][j-1] && ch == s[j-1]
是否为true
。
最终结果
dp[p.size()][s.size()]
显然,这样做可以得到答案,但是时间复杂度达到 O(len(s) * len(s) * len(p))
,空间复杂度达到 O( (len(s) + 1) * (len(p) + 1) )
。
0x02 动态规划改进版本
注意到,整个状态转移过程中,dp[i][0...j]
的状态仅取决于 dp[i-1][0...j]
,这句话有两个含义:
- 从行的角度,第 i 行的求解仅取决于 第 i-1 行
- 从列的角度,第 j 列的求解仅取决于 第 0…j 列
因此,我们可以很方便的使用滚动数组将空间压缩至一维
,只要将 j 的枚举顺序反过来
即可,这样求解 dp[j]
时,dp[0...j
]都是没有被修改过的旧值,数据依赖关系没有被破坏。
对于遇到 ‘*’
时,对于 dp[j]
,只要 dp[0...j]
中有true
,即可认为匹配成功。这个过程显然是单调的,不需要每次都从 0 枚举到 j ,因此一路或到最后一个j 即可。
下面贴一段改进后的代码:
bool isMatch(string s, string p) {
int n = s.size();
vector<bool> tp(n + 1, false);
tp[0] = true;
for(int i=0;i<p.size();++i){
char ch = p[i];
if(ch == '*'){
bool tmp=false;
for(int j=0;j<=n;++j){
tmp |= tp[j];
tp[j] = tmp;
}
continue;
}
for(int j=n;j>0;--j){
if(ch == '?' or ch == s[j-1]){
tp[j] = tp[j-1];
}else{
tp[j] = false;
}
}
tp[0]=false;
}
return tp[n];
}
此时空间复杂度 O(s.size() + 1)
,时间复杂度 O( (s.size() + 1) * (p.size() + 1))
,
虽然经过了优化,但是这个方法仍然没有暴力+回溯更快。其实仔细分析可以看到动态规划在逐步递推求解
的过程中,需要求解所有子问题
并cache子问题的答案
,因此这是动态规划的本质决定的。
到这里基本分析出了问题的关键,如果暴力+回溯的过程中没有重复求解子问题
,那么求解答案的速度可能会比动态规划更快。暴力+回溯的代码就不贴了,大家可以自行尝试或者阅读网上的其他作者的代码。
0x03 总结&题外话
事实上,任何方法,比如搜索(深度优先、广度优先等),在求解问题时只要不出现重复求解子问题,都有可能会比动态规划更快,尤其是在搜索过程中增加各种剪枝和优化。每个不同的求解方法都有自己特别擅长的问题,希望大家在解题或者面试时遇到问题不要迷恋一种解法,多思考。
给大家分享另外一个问题,这是一个搜索比动态规划更快
的例子,大家可都试试。题目链接:https://leetcode.com/problems/combination-sum/description/
附笔者的代码,同样使用了滚动数组优化的思路,但是速度同样没有搜索更快:
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
int n = candidates.size();
vector< vector< vector<int>>> tp(target+1, vector<vector<int>>());
for(int i=0;i<n;++i){
int can = candidates[i];
for(int j=target;j>=0; --j){
vector<vector<int>> tmp;
for(int k=0;k*can <= j;++k){
if(k*can == j){
vector<int> x;
for(int kk=1; kk<=k;++kk){
x.push_back(can);
}
tmp.emplace_back(move(x));
continue;
}
auto & p = tp[j - k * can];
for(auto x: p){
for(int kk=1; kk<=k;++kk){
x.push_back(can);
}
tmp.emplace_back(move(x));
}
}
tp[j] = move(tmp);
}
}
return tp[target];
}
};
好了今天就写到这里吧,谢谢大家关注与支持~
欢迎大家关注作者公众号,一起进步~