1. 单词拆分问题
2. 确定解题思路
经过思考,发现这是一个搜索问题。每次从s开头截取一个子串s’,如果s’在字典中,则继续搜索s剩余的部分;如果没在就不需要往下搜了。
根据这个思路,可得代码如下:
class Solution {
boolean able = false;
public void search(String s, Set<String> set){
if(able) return;
if(s.length() == 0){
able = true;
return;
}
for(int i = 1; i <= s.length(); i++){
if(set.contains(s.substring(0, i))){
search(s.substring(i), set);
}
}
}
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> set = new HashSet<>();
for(String word: wordDict){
set.add(word);
}
search(s, set);
return able;
}
}
但是提交的时候,超时了。超时的测试例是:
s = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"
wordDict = ["a","aa","aaa","aaaa","aaaaa","aaaaaa","aaaaaaa","aaaaaaaa","aaaaaaaaa","aaaaaaaaaa"]
3. 第一次修改
因为看到都是a,最后一个b,因为这个题的题意是只要找到任意一个满足的分割方法即可,我的朴素的想法就是先每次截取最长的子串,然后再依次递减,这样可以快速搜到。因此代码改为了:
int length = Math.min(max, s.length());
for(int i = length; i > 0; i--){//每次尽可能从最长的单词开始截取
if(set.contains(s.substring(0, i))){
search(s.substring(i), set, max);
}
}
但是还是超时了。
4. 第2次修改
发现其实完全没必要每次截取都从最长的子串截到最短,因为你截的子串最终是要在字典中进行查找的,那么子串的长度应该被限制在[max, min]之中,max,min分别表示字典中最长单词的长度和最短单词的长度。于是代码进一步改为:
int length = Math.min(max, s.length());
for(int i = length; i >= min; i--){//i是截取的单词长度,从最长开始到最短
if(set.contains(s.substring(0, i))){
search(s.substring(i), set, max, min);
}
}
仍然超时了。
5. 第3次修改
画了一个搜索树:
然后才发现,最关键的问题是重复的子问题太多了,那就将已经搜索过发现不能分开的子串存起来,以便于再次搜到这里时,直接返回此路不通的标识,而不再继续搜:
class Solution {
boolean able = false;
public void search(String s, Set<String> set, int max, int min, Set<String> not){
if(able) return;
if(s.length() == 0){
able = true;
return;
}
int length = Math.min(max, s.length());
for(int i = length; i >= min; i--){//i是截取的单词长度,从最长开始到最短
if(set.contains(s.substring(0, i))){
if(!not.contains(s.substring(i)))//只有当子串可能分时才进行搜索
search(s.substring(i), set, max, min, not);
}
}
if(!able){//如果搜完该子树,able没变为真,说明该子树不能分
not.add(s);
}
}
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> set = new HashSet<>();
Set<String> not = new HashSet<>();
int max = 0, min = 0x7fffffff;
for(String word: wordDict){
set.add(word);
if(word.length() > max) max = word.length();
if(word.length() < min) min = word.length();
}
search(s, set, max, min, not);
return able;
}
}
这一次终于通过了,并且效果还挺好,超过了94%的人。
6. 总结
前几次提交的代码,都是朴素的想法去提高效率,而没有想这其中的道理:
- 第一次修改背后隐含的思想。首先可以知道这棵搜索树是不平衡的,也就是有的叶子离根很近,深度浅,很快就能搜到,有的叶子离根很远,深度深,要搜很多次。因此第一次修改其实是将搜索从先搜最深处的然后再搜浅处的,这样可以节省一定的时间,但是有一个问题,就是如果s不能被分,那最终还是要搜完整棵树,这样改只能在平均时间上加快搜索成功的时候的时间,注意是平均时间,因为假如符合条件的叶子就在深处,那么自然还是从深处开始的好,但是我们不知道的情况下,就只能默认每一个叶子出现的概率是一致的。
- 第二次修改背后隐含的思想。按照上面的方法,我们的搜索树的宽度为s.length(),因为每个结点可以截取的子串长度为s.length(),…,1,对于每一个子串都要进行一次搜索,树的深度为s.length()/1。而修改后,树的宽度变成了(max - min + 1),树的深度为 ⌈ s . l e n g t h ( ) / m i n ⌉ \lceil s.length()/min\rceil ⌈s.length()/min⌉,这样其实一定程度上减小了搜索树的大小,也就是减小了搜索空间。
- 但是上面的修改都没有触及到核心问题,就是子问题重复,导致了需要做大量的重复性工作。因此第3次修改,使用了一个not集合,保存了那些已经搜索过但是此路不通(即该子树不能够被分割)的子树(子串)。这样的话,再遇到相同的子问题,就可以不用再对其进行搜索了,从而大大减少了工作量,这样就高效通过了。
综上,以后遇到类似的搜索问题时,要养成先分析搜索空间的习惯,然后查看搜索类型,是只搜到一个正确的叶子就返回还是要全树搜,再然后查看是否有重复子问题,有的话就要想办法进行优化(一般都是空间换时间)。