動態規劃 | 帶有通配符的字符串匹配(淺顯易懂)

帶有通配符的字符串匹配

一、Leetcode | 44 Wildcard Matching(只有一個字符串包含通配符)

這裏寫圖片描述


題目很簡單,就是說兩個字符串,一個含有通配符,去匹配另一個字符串;輸出兩個字符串是否一致。

注意:’?’表示匹配任意一個字符,’*’表示匹配任意字符0或者多次

首先,我們想到暴力破解。如果從頭到尾的破解,到第二個字符時,是否匹配成功取決於第一個字符是否匹配成功! 所以我們想到應該要用到動態規劃;

既然用到動態規劃,最重要的是設置初值找到遞推式:

於是,我們開始分析初值怎麼設;其實很簡單,把這個匹配問題可以想象成一個矩陣dp,縱軸代表含有通配符的匹配字符串s2, 橫軸代表要匹配的字符串s1。假設現在s2=”a*b”, s1=”abc” 如圖:

這裏寫圖片描述

對應空位就是截止到當前的 (i,j) 位置,兩字符串是否匹配。匹配爲 T(true),不匹配爲 F(false),最後返回最右下角的值,就是當前兩個字符串是否匹配的最終值;

現在我們要做的設置初值,所以我們大可多加一行和一列,來填充初值;s1既然是要匹配的,我們都設爲 F(即dp[0][1]=F,dp[0][2]=F,dp[0][3]=F),表示當前還未開始匹配。而s2的初值,我們發現如果星號和a調換位置,星號可以匹配任意字符串,所以dp[i][0]的值取決於該位置是否爲星號和上一個位置d[i-1][0]是否爲T(其實就是上一個位置是否也是星號),所以我們設置dp[0][0]爲 T。所以形成下圖:

這裏寫圖片描述

此時初值已經設置完畢,我們要找到遞推式;經局部推算,我們發現遞推式應該有兩種,一種是當s2的字符是星號,另一種是s2的字符是非星號。

先看星號的情況:當要計算dp[2][1](即要匹配a*和a時),我們發現是取決於dp[1][1](即a和a是否匹配),當要計算dp[2][2] (即要匹配a*和ab時),是取決於dp[2][1] (即a*和a是否匹配)。抽象一下,星號和任意字符(0或多個)都匹配。所以字符串截止到星號匹配的情況,取決於當前位置向上和向左的情況(即可以爲0個字符,也可以爲多個字符)。所以此時遞推式爲dp[i][j]=dp[i1][j]||dp[i][j1] 如圖:

這裏寫圖片描述

再看非星號的情況:當要計算dp[3][2] (即要匹配a*b和ab時),則取決於dp[2][1]和a[3][2] (即a*和a是否匹配,同時b和b是否匹配);所以可以得到遞推式 dp[i][j] = dp[i-1][j-1]&&a[i][j]。如圖:

這裏寫圖片描述

最後我們得到了初值和兩個遞推式,就可以上代碼了;

//isMatch: s1無通配符,s2有通配符, '?'表示匹配任意一個字符,'*'表示匹配任意字符0或者多次
    public static boolean isMatch(String s1, String s2) {
        int countXing = 0;
        for(char c : s2.toCharArray())
            countXing++;
        if(s2.length() - countXing > s1.length() ) //說明s2去掉通配符,長度也長於s1
            return false;

        //動態規劃設置初值
        boolean[][] dp = new boolean[s2.length()+1][s1.length()+1]; 
        dp[0][0] = true;

        for(int i=1; i<=s2.length(); i++) {
            char s2_char = s2.charAt(i-1);
            dp[i][0] = dp[i-1][0] && s2_char=='*'; //設置每次循環的初值,即當星號不出現在首位時,匹配字符串的初值都爲false
            for(int j=1; j<=s1.length(); j++) {
                char s1_char = s1.charAt(j-1);
                if(s2_char == '*') 
                    dp[i][j] = dp[i-1][j] || dp[i][j-1]; //動態規劃遞推式(星號) 表示星號可以匹配0個(決定於上次外循環的結果)或者多個(決定於剛纔內循環的結果)
                else 
                    dp[i][j] = dp[i-1][j-1] && (s2_char=='?' || s1_char == s2_char); //動態規劃遞推式(非星號) 表示dp值取決於上次的狀態和當前狀態
            }
        }
        return dp[s2.length()][s1.length()];
    }

PS: 兩個字符串都包含通配符的解法

通過上面那個列子,其實就這個問題就很容易想了。

首先就是初值的設置,兩個字符串都按上題中的包含通配符的字符串設置初值的方法,根據是否爲星號和上一個的狀態。

其次就是遞推式,它不用變,只是需要同時判斷兩個字符串中是否都包含通配符。

直接上代碼:

public static boolean isMatchByBoth(String s1, String s2) {

            //動態規劃設置初值
            boolean[][] dp = new boolean[s2.length()+1][s1.length()+1]; 
            dp[0][0] = true;

            for(int i=1; i<=s2.length(); i++) {
                char s2_char = s2.charAt(i-1);
                dp[i][0] = dp[i-1][0] && s2_char=='*'; //設置每次循環的初值,即當星號不出現在首位時,匹配字符串的初值都爲false
                for(int j=1; j<=s1.length(); j++) {
                    char s1_char = s1.charAt(j-1);
                    dp[0][j] = dp[0][j-1] && s1.charAt(j-1)=='*';
                    if(s2_char == '*' || s1_char == '*') {
                        dp[i][j] = dp[i-1][j] || dp[i][j-1]; //動態規劃遞推式(星號) 表示星號可以匹配0個(決定於上次外循環的結果)或者多個(決定於剛纔內循環的結果)
                    } else {
                        dp[i][j] = dp[i-1][j-1] && (s1_char=='?' || s2_char=='?' || s1_char == s2_char);
                    }
                }
            }
            return dp[s2.length()][s1.length()];
        }

二、Leetcode | 10 Regular Expression Matching(正則通配符)


這道題是把*的概念變了,它代表匹配星號之前元素的0個或多個。即 c* 帶便0個或者多個c。

所以具體代碼和思路寫到代碼註釋裏了。大家可以對照上面的題看看。

public class Solution {
    public boolean isMatch(String s, String p) {
       //有兩個假設,一個是不會出現c**的格式;第二個打頭的一定是字母
       //dp[i, j] means matching status between s.Substring(0, j) and p.Substring(0, i)
       boolean[][] dp = new boolean[p.length()+1][s.length()+1];
       dp[0][0] = true;

       for(int i=1; i<=p.length(); i++) {
           char pchar = p.charAt(i-1);
           //dp[i, 0] means if patter.Substring(0, i) matches empty string
           if(i > 1 && pchar=='*') dp[i][0] = dp[i-2][0]; 

           for(int j=1; j<=s.length(); j++) { 
               char schar = s.charAt(j-1);
               if(i > 1 && pchar == '*') {
                   //p可以匹配多個或0個pchar元素,所以檢查上一個,是是否匹配多個,檢查上上一個,是匹配了0個,檢查上上一個匹配元素的狀態(這是豎着的)
                   dp[i][j] = dp[i-2][j] || dp[i-1][j];
                   if(j > 1 && match(schar, p.charAt(i-2))) //從第二列(p的第二個字符開始),是否有連續匹配
                        dp[i][j] = dp[i][j] || dp[i][j-1];
               } else {
                   dp[i][j] = match(schar, pchar) && dp[i-1][j-1];
               }

           }
       }
       return dp[p.length()][s.length()];
    }

    boolean match(char c, char p) {
        if (p == '.') return true;
        else return c == p;
    }
}

以上只是自己的理解,希望大家多多交流!

發佈了54 篇原創文章 · 獲贊 26 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章