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];
}
};
好了今天就寫到這裏吧,謝謝大家關注與支持~
歡迎大家關注作者公衆號,一起進步~