题目描述(中等难度)

139. Word Break - 图1

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

解法一 回溯

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

  1. public boolean wordBreak(String s, List<String> wordDict) {
  2. return wordBreakHelper(s,wordDict,"");
  3. }
  4. //temp 是当前生成的字符串
  5. private boolean wordBreakHelper(String s, List<String> wordDict, String temp) {
  6. //如果此时生成的字符串长度够了,就判断和目标字符日是否相等
  7. if(temp.length() == s.length()){
  8. if(temp.equals(s)){
  9. return true;
  10. }else{
  11. return false;
  12. }
  13. }
  14. //长度超了,就返回 false
  15. if(temp.length() > s.length()){
  16. return false;
  17. }
  18. //考虑每个单词
  19. for(int i = 0;i < wordDict.size(); i++){
  20. if(wordBreakHelper(s,wordDict,temp + wordDict.get(i))){
  21. return true;
  22. }
  23. }
  24. return false;
  25. }

意料之中,超时了

139. Word Break - 图2

让我们考虑优化的方法。

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

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

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

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

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

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

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

  1. public boolean wordBreak(String s, List<String> wordDict) {
  2. return wordBreakHelper(s, wordDict, "");
  3. }
  4. private boolean wordBreakHelper(String s, List<String> wordDict, String temp) {
  5. if (temp.length() > s.length()) {
  6. return false;
  7. }
  8. //判断此时对应的字符是否全部相等
  9. for (int i = 0; i < temp.length(); i++) {
  10. if (s.charAt(i) != temp.charAt(i)) {
  11. return false;
  12. }
  13. }
  14. if (s.length() == temp.length()) {
  15. return true;
  16. }
  17. for (int i = 0; i < wordDict.size(); i++) {
  18. if (wordBreakHelper(s, wordDict, temp + wordDict.get(i))) {
  19. return true;
  20. }
  21. }
  22. return false;
  23. }

遗憾的是,依旧是超时

139. Word Break - 图3

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

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

所以代码可以继续优化。

  1. public boolean wordBreak(String s, List<String> wordDict) {
  2. HashSet<Character> set = new HashSet<>();
  3. //将 wordDict 的每个字母放到 set 中
  4. for (int i = 0; i < wordDict.size(); i++) {
  5. String t = wordDict.get(i);
  6. for (int j = 0; j < t.length(); j++) {
  7. set.add(t.charAt(j));
  8. }
  9. }
  10. //判断 s 的每个字母在 set 中是否存在
  11. for (int i = 0; i < s.length(); i++) {
  12. if (!set.contains(s.charAt(i))) {
  13. return false;
  14. }
  15. }
  16. return wordBreakHelper(s, wordDict, "");
  17. }
  18. private boolean wordBreakHelper(String s, List<String> wordDict, String temp) {
  19. if (temp.length() > s.length()) {
  20. return false;
  21. }
  22. for (int i = 0; i < temp.length(); i++) {
  23. if (s.charAt(i) != temp.charAt(i)) {
  24. return false;
  25. }
  26. }
  27. if (s.length() == temp.length()) {
  28. return true;
  29. }
  30. for (int i = 0; i < wordDict.size(); i++) {
  31. if (wordBreakHelper(s, wordDict, temp + wordDict.get(i))) {
  32. return true;
  33. }
  34. }
  35. return false;
  36. }

令人悲伤的是

139. Word Break - 图4

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

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

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

  1. public boolean wordBreak(String s, List<String> wordDict) {
  2. HashSet<Character> set = new HashSet<>();
  3. for (int i = 0; i < wordDict.size(); i++) {
  4. String t = wordDict.get(i);
  5. for (int j = 0; j < t.length(); j++) {
  6. set.add(t.charAt(j));
  7. }
  8. }
  9. for (int i = 0; i < s.length(); i++) {
  10. if (!set.contains(s.charAt(i))) {
  11. return false;
  12. }
  13. }
  14. return wordBreakHelper(s, wordDict, "", new HashMap<String,Boolean>());
  15. }
  16. private boolean wordBreakHelper(String s, List<String> wordDict, String temp, HashMap<String, Boolean> hashMap) {
  17. if (temp.length() > s.length()) {
  18. return false;
  19. }
  20. //之前是否存过
  21. if(hashMap.containsKey(temp)){
  22. return hashMap.get(temp);
  23. }
  24. for (int i = 0; i < temp.length(); i++) {
  25. if (s.charAt(i) != temp.charAt(i)) {
  26. return false;
  27. }
  28. }
  29. if (s.length() == temp.length()) {
  30. return true;
  31. }
  32. for (int i = 0; i < wordDict.size(); i++) {
  33. if (wordBreakHelper(s, wordDict, temp + wordDict.get(i), hashMap)) {
  34. //结果放入 hashMap
  35. hashMap.put(temp, true);
  36. return true;
  37. }
  38. }
  39. //结果放入 hashMap
  40. hashMap.put(temp, false);
  41. return false;
  42. }

这次就成功通过了。

解法二 分治

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

这个想法前边已经做过很多很多题了,大家可以参考 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 构成。

那么我们就可以知道

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

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

  1. public boolean wordBreak(String s, List<String> wordDict) {
  2. HashSet<String> set = new HashSet<>();
  3. for (int i = 0; i < wordDict.size(); i++) {
  4. set.add(wordDict.get(i));
  5. }
  6. return wordBreakHelper(s, set);
  7. }
  8. private boolean wordBreakHelper(String s, HashSet<String> set) {
  9. if (s.length() == 0) {
  10. return true;
  11. }
  12. for (int i = 0; i < s.length(); i++) {
  13. if (set.contains(s.substring(i, s.length())) && wordBreakHelper(s.substring(0, i), set)) {
  14. return true;
  15. }
  16. }
  17. return false;
  18. }

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

139. Word Break - 图5

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

  1. public boolean wordBreak(String s, List<String> wordDict) {
  2. HashSet<String> set = new HashSet<>();
  3. for (int i = 0; i < wordDict.size(); i++) {
  4. set.add(wordDict.get(i));
  5. }
  6. return wordBreakHelper(s, set, new HashMap<String, Boolean>());
  7. }
  8. private boolean wordBreakHelper(String s, HashSet<String> set, HashMap<String, Boolean> map) {
  9. if (s.length() == 0) {
  10. return true;
  11. }
  12. if (map.containsKey(s)) {
  13. return map.get(s);
  14. }
  15. for (int i = 0; i < s.length(); i++) {
  16. if (set.contains(s.substring(i, s.length())) && wordBreakHelper(s.substring(0, i), set, map)) {
  17. map.put(s, true);
  18. return true;
  19. }
  20. }
  21. map.put(s, false);
  22. return false;
  23. }

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

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

  1. public boolean wordBreak(String s, List<String> wordDict) {
  2. HashSet<String> set = new HashSet<>();
  3. for (int i = 0; i < wordDict.size(); i++) {
  4. set.add(wordDict.get(i));
  5. }
  6. boolean[] dp = new boolean[s.length() + 1];
  7. dp[0] = true;
  8. for (int i = 1; i <= s.length(); i++) {
  9. for (int j = 0; j < i; j++) {
  10. dp[i] = dp[j] && set.contains(s.substring(j, i));
  11. if (dp[i]) {
  12. break;
  13. }
  14. }
  15. }
  16. return dp[s.length()];
  17. }

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

windliang wechat

添加好友一起进步~

如果觉得有帮助的话,可以点击 这里 给一个 star 哦 ^^

如果想系统的学习数据结构和算法,强烈推荐一个我之前学过的课程,可以点击 这里 查看详情