题目描述(中等难度)

给一个字符串,和一些单词,问字符串能不能由这些单词构成。每个单词可以用多次,也可以不用。

解法一 回溯

来一个简单粗暴的方法,利用回溯法,用 wordDict 去生成所有可能的字符串。期间如果出现了目标字符串 s,就返回 true

public boolean wordBreak(String s, List<String> wordDict) { return wordBreakHelper(s,wordDict,""); } //temp 是当前生成的字符串 private boolean wordBreakHelper(String s, List<String> wordDict, String temp) { //如果此时生成的字符串长度够了,就判断和目标字符日是否相等 if(temp.length() == s.length()){ if(temp.equals(s)){ return true; }else{ return false; } } //长度超了,就返回 false if(temp.length() > s.length()){ return false; } //考虑每个单词 for(int i = 0;i < wordDict.size(); i++){ if(wordBreakHelper(s,wordDict,temp + wordDict.get(i))){ return true; } } return false; } 

意料之中,超时了

让我们考虑优化的方法。

在递归出口的地方优化一下。

之前是在长度相等的时候,开始判断字符串是否相等。

很明显,字符串长度相等之前我们其实就可以判断当前是不是符合了。

例如 temp = "abc",如果 s = "dddefg",虽然此时 temps 的长度不相等。但因为前缀已经不同,所以后边无论是什么都不可以了。此时就可以返回 false 了。

所以递归出口可以从头判断每个字符是否相等,不相等就直接返回 false

for (int i = 0; i < temp.length(); i++) { if (s.charAt(i) != temp.charAt(i)) { return false; } } 

然后代码就是下边的样子。

public boolean wordBreak(String s, List<String> wordDict) { return wordBreakHelper(s, wordDict, ""); } private boolean wordBreakHelper(String s, List<String> wordDict, String temp) { if (temp.length() > s.length()) { return false; } //判断此时对应的字符是否全部相等 for (int i = 0; i < temp.length(); i++) { if (s.charAt(i) != temp.charAt(i)) { return false; } } if (s.length() == temp.length()) { return true; } for (int i = 0; i < wordDict.size(); i++) { if (wordBreakHelper(s, wordDict, temp + wordDict.get(i))) { return true; } } return false; } 

遗憾的是,依旧是超时

发现上边的例子答案很明显是 false,因为 s 中的 b 字母在 wordDict 中并没有出现。

所以我们可以先遍历一遍 swordDict ,从而确定 s 中的字符是否在 wordDict 中存在,如果不存在可以提前返回 false

所以代码可以继续优化。

public boolean wordBreak(String s, List<String> wordDict) { HashSet<Character> set = new HashSet<>(); //将 wordDict 的每个字母放到 set 中 for (int i = 0; i < wordDict.size(); i++) { String t = wordDict.get(i); for (int j = 0; j < t.length(); j++) { set.add(t.charAt(j)); } } //判断 s 的每个字母在 set 中是否存在 for (int i = 0; i < s.length(); i++) { if (!set.contains(s.charAt(i))) { return false; } } return wordBreakHelper(s, wordDict, ""); } private boolean wordBreakHelper(String s, List<String> wordDict, String temp) { if (temp.length() > s.length()) { return false; } for (int i = 0; i < temp.length(); i++) { if (s.charAt(i) != temp.charAt(i)) { return false; } } if (s.length() == temp.length()) { return true; } for (int i = 0; i < wordDict.size(); i++) { if (wordBreakHelper(s, wordDict, temp + wordDict.get(i))) { return true; } } return false; } 

令人悲伤的是

还有 5test 没有通过。还有什么可以优化的地方呢?

是时候拿出绝招了,在前边的题已经用过很多很多次,memoization 技术。思想就是把回溯中已经考虑过的解存起来,第二次回溯过来的时候可以直接使用。

这里的话,我们可以用一个 HashMapkey 的话就存 tempvalue 的话就代表以当前 temp 开始的字符串,经过后边的尝试是否能达到目标字符串 s

public boolean wordBreak(String s, List<String> wordDict) { HashSet<Character> set = new HashSet<>(); for (int i = 0; i < wordDict.size(); i++) { String t = wordDict.get(i); for (int j = 0; j < t.length(); j++) { set.add(t.charAt(j)); } } for (int i = 0; i < s.length(); i++) { if (!set.contains(s.charAt(i))) { return false; } } return wordBreakHelper(s, wordDict, "", new HashMap<String,Boolean>()); } private boolean wordBreakHelper(String s, List<String> wordDict, String temp, HashMap<String, Boolean> hashMap) { if (temp.length() > s.length()) { return false; } //之前是否存过 if(hashMap.containsKey(temp)){ return hashMap.get(temp); } for (int i = 0; i < temp.length(); i++) { if (s.charAt(i) != temp.charAt(i)) { return false; } } if (s.length() == temp.length()) { return true; } for (int i = 0; i < wordDict.size(); i++) { if (wordBreakHelper(s, wordDict, temp + wordDict.get(i), hashMap)) { //结果放入 hashMap hashMap.put(temp, true); return true; } } //结果放入 hashMap hashMap.put(temp, false); return false; } 

这次就成功通过了。

解法二 分治

换一种思想,分治,也就是大问题转换为小问题,通过小问题来解决。

这个想法前边已经做过很多很多题了,大家可以参考 97 题115 题 等等。

我们现在要判断目标串 s 是否能由 wordDict 构成。

我们用 dp[i,j),表示从 s 的第 i 个字符开始,到第 j 个字符的前一个结束的字符串是否能由 wordDict 构成。

假如我们知道了 dp[0,1) dp[0,2) dp[0,3)...dp[0,len - 1) ,也就是除 s 本身的所有子串是否能由 wordDict 构成。

那么我们就可以知道

dp[0,len) = dp[0,1) && wordDict.contains(s[i,len)) || dp[0,2) && wordDict.contains(s[2,len)) || dp[0,3) && wordDict.contains(s[3,len)) ... || dp[0,len - 1) && wordDict.contains(s[len - 1,len)) 

dp[0,len) 就代表着 s 是否能由 wordDict 构成。有了上边的转移方程,就可以用递归写出来了。

public boolean wordBreak(String s, List<String> wordDict) { HashSet<String> set = new HashSet<>(); for (int i = 0; i < wordDict.size(); i++) { set.add(wordDict.get(i)); } return wordBreakHelper(s, set); } private boolean wordBreakHelper(String s, HashSet<String> set) { if (s.length() == 0) { return true; } for (int i = 0; i < s.length(); i++) { if (set.contains(s.substring(i, s.length())) && wordBreakHelper(s.substring(0, i), set)) { return true; } } return false; } 

如果不做任何处理,依旧会得到超时。

所有,memoization 又来了,和之前一样将中间结果存储起来。

public boolean wordBreak(String s, List<String> wordDict) { HashSet<String> set = new HashSet<>(); for (int i = 0; i < wordDict.size(); i++) { set.add(wordDict.get(i)); } return wordBreakHelper(s, set, new HashMap<String, Boolean>()); } private boolean wordBreakHelper(String s, HashSet<String> set, HashMap<String, Boolean> map) { if (s.length() == 0) { return true; } if (map.containsKey(s)) { return map.get(s); } for (int i = 0; i < s.length(); i++) { if (set.contains(s.substring(i, s.length())) && wordBreakHelper(s.substring(0, i), set, map)) { map.put(s, true); return true; } } map.put(s, false); return false; } 

当然除了递归中存储,我们也可以直接用动态规划的思想,求一个结果就保存一个结果。

dp[i] 表示字符串 s[0,i) 能否由 wordDict 构成。

public boolean wordBreak(String s, List<String> wordDict) { HashSet<String> set = new HashSet<>(); for (int i = 0; i < wordDict.size(); i++) { set.add(wordDict.get(i)); } boolean[] dp = new boolean[s.length() + 1]; dp[0] = true; for (int i = 1; i <= s.length(); i++) { for (int j = 0; j < i; j++) { dp[i] = dp[j] && set.contains(s.substring(j, i)); if (dp[i]) { break; } } } return dp[s.length()]; } 

解法一的回溯优化主要就是剪枝,让一些提前知道结果的解直接结束,不进入递归。解法二的想法,就太常用了,从递归到 memoization 再到动态规划,其实本质都是一样的。

results matching ""

    No results matching ""