動態規劃五:單詞拆分

本文正在參加「Java主題月 - Java 刷題打卡」,詳情查看活動連接java

1、題目單詞拆分

給定一個非空字符串 s 和一個包含非空單詞的列表 wordDict,斷定 s 是否能夠被空格拆分爲一個或多個在字典中出現的單詞。算法

  • 說明:
    • 拆分時能夠重複使用字典中的單詞。
    • 你能夠假設字典中沒有重複的單詞。
  • 示例:
    • 輸入: s = "leetcode", wordDict = ["leet", "code"]
    • 輸出: true
    • 解釋: 返回 true 由於 "leetcode" 能夠被拆分紅 "leet code"。

2、分析

若是字符串s能被拆分爲多個單詞,則必定存在j使得 s.substring(j)s.substring(j,slen+1)兩個子串都能被拆分爲多個單詞。同理,兩個子串也存在於字符串s相同的性質。則咱們能夠從0遍歷 s.length(),記錄每一個位置以前的子串是否可以被拆分爲多個單詞。數組

肯定dp數組及下標含義

dp[i]: s.substring(i) 可否被拆分爲單詞。注:dp[i] 記錄的應該是字符串下標爲 i-1 的位置能否能拆分爲多個單詞的狀況。markdown

肯定遞推公式

對於dp[i],若是 s.substring(i) 能被拆分爲多個單詞,則必定存在j使得 s.substring(j) 子串能被拆分爲多個單詞,以及 s.substring(j,i) 子串爲一個單詞。因此有,dp[i] = dp[j] && wordDict.contains(s.substring(j,i))oop

dp數組初始化

dp[0]表明的是空子串的狀況,題目中指出,給定一個非空字符串,因此空子串在本題中應該是無心義的。可是,dp[i]須要依靠dp[j]肯定,若是dp[0]爲false,則後面dp[i]是否爲true都將無心義,因此dp[0]必須爲true。post

遍歷順序

本題使用兩層循環遍歷,外層循環天然是從0遍歷到s.length()。對於內層,從i遍歷到1能明顯減小循環次數。spa

舉例推導dp數組

139_0.png

3、代碼實現

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        Set<String> wordSet = new HashSet<>(wordDict);
        int sLength = s.length();
        boolean[] dp = new boolean[sLength+1];
        dp[0] = true;
        for(int i = 1; i <= sLength; i++){
            for (int j = i; j >= 0; j--){
                if(dp[j] && wordSet.contains(s.substring(j,i))){
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[sLength];
    }
}
複製代碼

4、進階

140.單詞拆分IIcode

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class Solution {

    public List<String> wordBreak(String s, List<String> wordDict) {
        Set<String> wordSet = new HashSet<>(wordDict);
        int len = s.length();

        // 第 1 步:動態規劃計算是否有解
        boolean[] dp = new boolean[len + 1];
        dp[0] = true;

        for (int right = 1; right <= len; right++) {
            // 從後向前遍歷是更快的
            for (int left = right - 1; left >= 0; left--) {
                if (wordSet.contains(s.substring(left, right)) && dp[left]) {
                    dp[right] = true;
                    break;
                }
            }
        }

        // 第 2 步:回溯算法搜索全部符合條件的解
        List<String> res = new ArrayList<>();
        if (dp[len]) {
            Deque<String> path = new ArrayDeque<>();
            dfs(s, len, wordSet, dp, path, res);
            return res;
        }
        return res;
    }

    /** * s[0:len) 若是能夠拆分紅 wordSet 中的單詞,把遞歸求解的結果加入 res 中 * * @param s * @param len 長度爲 len 的 s 的前綴子串 * @param wordSet 單詞集合,已經加入哈希表 * @param dp 預處理獲得的 dp 數組 * @param path 從葉子結點到根結點的路徑 * @param res 保存全部結果的變量 */
    private void dfs(String s, int len, Set<String> wordSet, boolean[] dp, Deque<String> path, List<String> res) {
        if (len == 0) {
            res.add(String.join(" ",path));
            return;
        }

        // 能夠拆分的左邊界從 len - 1 依次枚舉到 0
        for (int i = len - 1; i >= 0; i--) {
            String suffix = s.substring(i, len);
            if (wordSet.contains(suffix) && dp[i]) {
                path.addFirst(suffix);
                dfs(s, i, wordSet, dp, path, res);
                path.removeFirst();
            }
        }
    }
}
複製代碼
相關文章
相關標籤/搜索