单词拆分!
回溯法
之前的一道題目回溯算法:131分割回文串,就是枚舉字符串的所有分割情況。
回溯算法:分割回文串:是枚舉分割后的所有子串,判斷是否回文。
本道是枚舉分割所有字符串,判斷是否在字典里出現過。
所以回溯法C++代碼:
時間復雜度:O(2^n),因為每一個單詞都有兩個狀態,切割和不切割 空間復雜度:O(n),算法遞歸系統調用棧的空間 class Solution { private:bool backtracking (const string& s, const unordered_set<string>& wordSet, int startIndex) {if (startIndex >= s.size()) {return true;}for (int i = startIndex; i < s.size(); i++) {string word = s.substr(startIndex, i - startIndex + 1);if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, i + 1)) {return true;}}return false;} public:bool wordBreak(string s, vector<string>& wordDict) {unordered_set<string> wordSet(wordDict.begin(), wordDict.end());return backtracking(s, wordSet, 0);} };遞歸的過程中有很多重復計算,可以使用數組保存一下遞歸過程中計算的結果。
這個叫做記憶化遞歸,這種方法我們之前已經提過很多次了。
使用memory數組保存每次計算的以startIndex起始的計算結果,如果memory[startIndex]里已經被賦值了,直接用memory[startIndex]的結果。
C++代碼如下:
時間復雜度其實也是:O(2^n) class Solution { private:bool backtracking (const string& s,const unordered_set<string>& wordSet,vector<int>& memory,int startIndex) {if (startIndex >= s.size()) {return true;}// 如果memory[startIndex]不是初始值了,直接使用memory[startIndex]的結果if (memory[startIndex] != -1) return memory[startIndex];for (int i = startIndex; i < s.size(); i++) {string word = s.substr(startIndex, i - startIndex + 1);if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, memory, i + 1)) {memory[startIndex] = 1; // 記錄以startIndex開始的子串是可以被拆分的return true;}}memory[startIndex] = 0; // 記錄以startIndex開始的子串是不可以被拆分的return false;} public:bool wordBreak(string s, vector<string>& wordDict) {unordered_set<string> wordSet(wordDict.begin(), wordDict.end());vector<int> memory(s.size(), -1); // -1 表示初始化狀態return backtracking(s, wordSet, memory, 0);} };完全背包
單詞就是物品,字符串s就是背包,單詞能否組成字符串s,就是問物品能不能把背包裝滿。
(通過觀察示例三,可以發現所有物品都必須放進背包,且每件物品至少一個)
拆分時可以重復使用字典中的單詞,說明就是一個完全背包!
確定dp數組以及下標的含義
dp[ j ] : 字符串長度為 j 的話,dp[ j ] 為true,表示可以拆分為一個或多個在字典中出現的單詞。
確定遞推公式
若dp[ i ] 是true(為true則表示可以拆分成字典中出現的單詞),且 [ i, j ] 這個區間的子串出現在字典里,那么dp[j]一定是true。(i < j )。
所以遞推公式是
if( [i, j] 這個區間的子串出現在字典里 && dp[j]==true ) 那么 dp[j] = true。
dp數組如何初始化
從遞歸公式中可以看出,dp[j] 的狀態依靠 dp[i]是否為true,那么dp[0]就是遞歸的根基,dp[0]一定要為true,否則遞歸下去后面都都是false了。
那么dp[0]有沒有意義呢?
dp[0]表示如果字符串為空的話,說明出現在字典里。
但題目中說了“給定一個非空字符串 s” 所以測試數據中不會出現i為0的情況,那么dp[0]初始為true完全就是為了推導公式。
下標非0的dp[i]初始化為false,只要沒有被覆蓋說明都是不可拆分為一個或多個在字典中出現的單詞。
確定遍歷順序
題目中說是拆分為一個或多個在字典中出現的單詞,所以這是完全背包。
還要討論兩層for循環的前后循序。
如果求組合數就是外層for循環遍歷物品,內層for遍歷背包。
如果求排列數就是外層for遍歷背包,內層for循環遍歷物品。
本題最終要求的是是否都出現過,所以對出現單詞集合里的元素是組合還是排列,并不在意!
即:外層for循環遍歷物品,內層for遍歷背包 或者 外層for遍歷背包,內層for循環遍歷物品 都是可以的。
但本題由于特殊性,因為是要求子串,最好是遍歷背包放在外循環,將遍歷物品放在內循環。
如果要是外層for循環遍歷物品,內層for遍歷背包,就需要把所有的子串都預先放在一個容器里。(如果不理解的話,可以自己嘗試這么寫一寫就理解了)
舉例推導dp[i]
以輸入: s = “leetcode”, wordDict = [“leet”, “code”]為例,dp狀態如圖:
總結