1. 程式人生 > 實用技巧 >139. [動態規劃][記憶化搜素]單詞拆分

139. [動態規劃][記憶化搜素]單詞拆分

139. 單詞拆分

方法一:動態規劃

我們定義\(dp[i]\)表示字串\(s\)\(i\)個字元組成的字串\(s[0…i-1]\)能否被拆分若干個\(wordDict\)中的單詞。

接下來要考慮狀態轉移方程,每次轉移時需要列舉包含位置 \(i-1\)的最後一個單詞,看它是否出現在\(wordDict\)中以及除去這部分的字串是否合法即可。

公式化來說,我們需要列舉\(s\)的分割點\(j\),看\(s\)分割出的\(s_i\)\(s_j\)是否合法,那麼他們重新拼接起來自然也合法。由於在迴圈中,我們已經計算出了\(dp[0…1]\)的值,因此字串\(s_1\)是否合法可以直接由\(dp[j]\)

得知,剩下我們只需要判斷\(s_2\)是否合法就行。

因此得出狀態轉移方程的公式為:\(dp[i]=dp[j]\ \&\& \ check(s[j..i−1])\)

其中\({check}(s[j…i-1])\) 表示子串 \(s[j…i-1]\)是否出現在\(wordDict\)中。

對於檢查一個字串是否出現在給定的字串列表裡一般可以考慮雜湊表來快速判斷,具體實現如下:

// 執行耗時:10 ms,擊敗了53.29% 的Java使用者
// 記憶體消耗:39 MB,擊敗了31.10% 的Java使用者

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        Set<String> wordDictSet = new HashSet(wordDict);
        boolean[] dp = new boolean[s.length() + 1];  // dp[i]表示從0-i位的子串能否拆分
        dp[0] = true;
        for (int i = 1; i <= s.length(); i++) {
            for (int j = 0; j < i; j++){
                if (dp[j] && wordDict.contains(s.substring(j, i))){
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

優化:

考慮到每次遍歷\(s\)字串,檢查是否在\(Set\)中存在,每次都是從頭遍歷到尾,這樣必定迴帶來大量的無效遍歷,如果當前遍歷的長度大於欄位中字串的最大長度,則一定不可能匹配成功,所以,\(dp[i]\)只需要往前探索到詞典裡最長的單詞即可。

// 執行耗時:1 ms,擊敗了99.68% 的Java使用者
// 記憶體消耗:36.4 MB,擊敗了99.80% 的Java使用者

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        int len = s.length(), maxw = 0;
        boolean[] dp = new boolean[len + 1];
        dp[0] = true;
        Set<String> wordDictSet = new HashSet();
        for(String str : wordDict){
            wordDictSet.add(str);
            maxw = Math.max(maxw, str.length());
        }
        for(int i = 1; i < len + 1; i++){
            for(int j = i; j >= 0 && j >= i - maxw; j--){
                if(dp[j] && wordDictSet.contains(s.substring(j, i))){
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[len];
    }
}