|
| 1 | +# 题目描述(困难难度) |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +[139 题](https://leetcode.wang/leetcode-139-Word-Break.html) 的升级版。给一个字符串,和一些单词,找出由这些单词组成该字符串的所有可能,每个单词可以用多次,也可以不用。 |
| 6 | + |
| 7 | +完全按照 [139 题](https://leetcode.wang/leetcode-139-Word-Break.html) 的思路做了,大家可以先过去看一下。 |
| 8 | + |
| 9 | +# 解法一 动态规划 |
| 10 | + |
| 11 | +先考虑 `139 题` 最后一个解法,动态规划,看起来也比较简单粗暴。 |
| 12 | + |
| 13 | +> 用 `dp[i]` 表示字符串 `s[0,i)` 能否由 `wordDict` 构成。 |
| 14 | +> |
| 15 | +> ```java |
| 16 | +> public boolean wordBreak(String s, List<String> wordDict) { |
| 17 | +> HashSet<String> set = new HashSet<>(); |
| 18 | +> for (int i = 0; i < wordDict.size(); i++) { |
| 19 | +> set.add(wordDict.get(i)); |
| 20 | +> } |
| 21 | +> boolean[] dp = new boolean[s.length() + 1]; |
| 22 | +> dp[0] = true; |
| 23 | +> for (int i = 1; i <= s.length(); i++) { |
| 24 | +> for (int j = 0; j < i; j++) { |
| 25 | +> dp[i] = dp[j] && wordDict.contains(s.substring(j, i)); |
| 26 | +> if (dp[i]) { |
| 27 | +> break; |
| 28 | +> } |
| 29 | +> } |
| 30 | +> } |
| 31 | +> return dp[s.length()]; |
| 32 | +> } |
| 33 | +> ``` |
| 34 | +
|
| 35 | +这里修改的话,我们只需要用 `dp[i]` 表示字符串 `s[0,i)` 由 `wordDict` 构成的所有情况。 |
| 36 | +
|
| 37 | +总体思想还是和之前一样的。 |
| 38 | +
|
| 39 | +```java |
| 40 | +X X X X X X |
| 41 | +^ ^ ^ |
| 42 | +0 j i |
| 43 | +先判断 j 到 i 的字符串在没在 wordDict 中 |
| 44 | +然后把 0 到 j 的字符串由 wordDict 构成所有情况后边加空格再加上 j 到 i 的字符串即可 |
| 45 | +``` |
| 46 | +
|
| 47 | +结合上边的思想,然后把它放到循环中,考虑所有情况即可。 |
| 48 | + |
| 49 | +```java |
| 50 | +public List<String> wordBreak(String s, List<String> wordDict) { |
| 51 | + HashSet<String> set = new HashSet<>(); |
| 52 | + for (int i = 0; i < wordDict.size(); i++) { |
| 53 | + set.add(wordDict.get(i)); |
| 54 | + } |
| 55 | + List<List<String>> dp = new ArrayList<>(); |
| 56 | + List<String> temp = new ArrayList<>(); |
| 57 | + //空串的情况 |
| 58 | + temp.add(""); |
| 59 | + dp.add(temp); |
| 60 | + for (int i = 1; i <= s.length(); i++) { |
| 61 | + temp = new ArrayList<>(); |
| 62 | + for (int j = 0; j < i; j++) { |
| 63 | + if (wordDict.contains(s.substring(j, i))) { |
| 64 | + //得到前半部分的所有情况然后和当前单词相加 |
| 65 | + for (int k = 0; k < dp.get(j).size(); k++) { |
| 66 | + String t = dp.get(j).get(k); |
| 67 | + //空串的时候不加空格,也就是 j = 0 的时候 |
| 68 | + if (t.equals("")) { |
| 69 | + temp.add(s.substring(j, i)); |
| 70 | + } else { |
| 71 | + temp.add(t + " " + s.substring(j, i)); |
| 72 | + } |
| 73 | + |
| 74 | + } |
| 75 | + |
| 76 | + } |
| 77 | + } |
| 78 | + dp.add(temp); |
| 79 | + } |
| 80 | + return dp.get(s.length()); |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +遗憾的是,熟悉的问题又来了。 |
| 85 | + |
| 86 | + |
| 87 | + |
| 88 | +由于 `s` 中的 `b` 字母在 `wordDict` 中并没有出现,所以其实我们并不需要做那么多循环,直接返回空列表即可。 |
| 89 | + |
| 90 | +和之前一样,所以我们可以先遍历一遍 `s` 和 `wordDict` ,从而确定 `s` 中的字符是否在 `wordDict` 中存在,如果不存在可以提前返回空列表。 |
| 91 | + |
| 92 | +```java |
| 93 | +public List<String> wordBreak(String s, List<String> wordDict) { |
| 94 | + //提前进行一次判断 |
| 95 | + HashSet<Character> set2 = new HashSet<>(); |
| 96 | + for (int i = 0; i < wordDict.size(); i++) { |
| 97 | + String t = wordDict.get(i); |
| 98 | + for (int j = 0; j < t.length(); j++) { |
| 99 | + set2.add(t.charAt(j)); |
| 100 | + } |
| 101 | + } |
| 102 | + for (int i = 0; i < s.length(); i++) { |
| 103 | + if (!set2.contains(s.charAt(i))) { |
| 104 | + return new ArrayList<>(); |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + HashSet<String> set = new HashSet<>(); |
| 109 | + for (int i = 0; i < wordDict.size(); i++) { |
| 110 | + set.add(wordDict.get(i)); |
| 111 | + } |
| 112 | + List<List<String>> dp = new ArrayList<>(); |
| 113 | + List<String> temp = new ArrayList<>(); |
| 114 | + temp.add(""); |
| 115 | + dp.add(temp); |
| 116 | + for (int i = 1; i <= s.length(); i++) { |
| 117 | + temp = new ArrayList<>(); |
| 118 | + for (int j = 0; j < i; j++) { |
| 119 | + if (wordDict.contains(s.substring(j, i))) { |
| 120 | + for (int k = 0; k < dp.get(j).size(); k++) { |
| 121 | + String t = dp.get(j).get(k); |
| 122 | + if (t.equals("")) { |
| 123 | + temp.add(s.substring(j, i)); |
| 124 | + } else { |
| 125 | + temp.add(t + " " + s.substring(j, i)); |
| 126 | + } |
| 127 | + |
| 128 | + } |
| 129 | + |
| 130 | + } |
| 131 | + } |
| 132 | + dp.add(temp); |
| 133 | + } |
| 134 | + return dp.get(s.length()); |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +遗憾的是,刚刚那个例子通过了,又出现了新的问题。 |
| 139 | + |
| 140 | + |
| 141 | + |
| 142 | +由于 `wordDict` 有 `b` 字母了,所以并没有提前结束,而是进了 `for` 循环。 |
| 143 | + |
| 144 | +再优化也想不到方法了,是我们的算法出问题了。因为 `139` 题中找到一个解以后就 `break` 了,而这里我们要考虑所有子串,所有的解,极端情况下时间复杂度达到了 `O(n³)`。还有一点致命的是,我们之前求的解最后可能并不需要。举个例子。 |
| 145 | + |
| 146 | +```java |
| 147 | +"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabad" |
| 148 | +["aa","aaa","aaaa","aaaaa","aaaaaa","aaaaaaa","aaaaaaaa","aaaaaaaaa","aaaaaaaaaa","b","ba","de"] |
| 149 | + |
| 150 | +我们之前求了 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 的所有组成可能,但由于剩余字符串 "bad" 不在 wordDict 中,所有之前求出来并没有用 |
| 151 | + |
| 152 | +又比如,我们之前求了 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" 的所有组成可能,但由于剩余字符串 "ad" 不在 wordDict 中,所有之前求出来也并没有用 |
| 153 | +``` |
| 154 | + |
| 155 | +针对这个问题,我们可以优化一下,也就是下边的解法二 |
| 156 | + |
| 157 | +# 解法二 |
| 158 | + |
| 159 | +我们直接用递归的方法,先判断当前字符串在不在 `wordDict` 中,如果在的话就递归的去求剩余字符串的所有组成可能。此外,吸取之前的教训,直接使用 `memoization` 技术,将递归过程中求出来的解缓存起来,便于之后直接用。 |
| 160 | + |
| 161 | +```java |
| 162 | +public List<String> wordBreak(String s, List<String> wordDict) { |
| 163 | + HashSet<String> set = new HashSet<>(); |
| 164 | + for (int i = 0; i < wordDict.size(); i++) { |
| 165 | + set.add(wordDict.get(i)); |
| 166 | + } |
| 167 | + return wordBreakHelper(s, set, new HashMap<String, List<String>>()); |
| 168 | +} |
| 169 | + |
| 170 | +private List<String> wordBreakHelper(String s, HashSet<String> set, HashMap<String, List<String>> map) { |
| 171 | + if (s.length() == 0) { |
| 172 | + return new ArrayList<>(); |
| 173 | + } |
| 174 | + if (map.containsKey(s)) { |
| 175 | + return map.get(s); |
| 176 | + } |
| 177 | + List<String> res = new ArrayList<>(); |
| 178 | + for (int j = 0; j < s.length(); j++) { |
| 179 | + //判断当前字符串是否存在 |
| 180 | + if (set.contains(s.substring(j, s.length()))) { |
| 181 | + //空串的情况,直接加入 |
| 182 | + if (j == 0) { |
| 183 | + res.add(s.substring(j, s.length())); |
| 184 | + } else { |
| 185 | + //递归得到剩余字符串的所有组成可能,然后和当前字符串分别用空格连起来加到结果中 |
| 186 | + List<String> temp = wordBreakHelper(s.substring(0, j), set, map); |
| 187 | + for (int k = 0; k < temp.size(); k++) { |
| 188 | + String t = temp.get(k); |
| 189 | + res.add(t + " " + s.substring(j, s.length())); |
| 190 | + } |
| 191 | + } |
| 192 | + |
| 193 | + } |
| 194 | + } |
| 195 | + //缓存结果 |
| 196 | + map.put(s, res); |
| 197 | + return res; |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +# 总 |
| 202 | + |
| 203 | +按理说其实可以直接就想到解法二,但受之前写的题的影响,这种分治的问题,都最终能转成动态规划,所以先写了动态规划的思路,想直接一步到位,没想到反而遇到了问题,很有意思,哈哈。原因就是你求子问题的代价很大,而动态规划就是要求所有的子问题。而解决最终问题的时候,一些子问题其实是没有必要的。 |
| 204 | + |
0 commit comments