模糊匹配 -- 關於暴力與動態規劃的思考 Leetcode 44. Wildcard Matching

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],

初始狀態:

  1. 顯然 dp[0][0] = true,空串匹配空串是可以的。
  2. 同時,dp[0][1...s.size()] = false,空串不能匹配任何非空串。

狀態轉移

對於任意 i (1<= i <= p.size()) 和 j (0 <= j <= s.size()),考察 ch = p[i-1]

  1. 如果 ch == '*'dp[i][j] 爲 true,取決於 dp[i-1][0...j] 中有沒有 true,意思是用 * 表示 s[0...j-1] 的任意後綴進行嘗試。
  2. 如果 ch == '?'dp[i][j] 爲 true,取決於 dp[i-1][j-1] 是否爲true
  3. 否則,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],這句話有兩個含義:

  1. 從行的角度,第 i 行的求解僅取決於 第 i-1 行
  2. 從列的角度,第 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];
    }
};

好了今天就寫到這裏吧,謝謝大家關注與支持~


歡迎大家關注作者公衆號,一起進步~
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章