从递归到动态规划,再到BFS
word-break
http://www.nowcoder.com/questionTerminal/5f3b7bf611764c8ba7868f3ed40d6b2c
方法1:递归
很明显,这是个递归的问题。我们应该能比较轻松的写出下面的代码:
import java.util.*; public class Solution { public boolean wordBreak(String s, Set<String> dict) { if (s.length()==0) return true; for (int i=1;i<=s.length();++i){ String sub=s.substring(0,i); if (dict.contains(sub)&&wordBreak(s.substring(i),dict)) return true; } return false; } }
但是这个代码在执行下面的测试样例(这个样例是在LeetCode上拿到的)时,会超时。
。
为什么会这样呢?想一个简单一点的,s的值为aaaaab
,dict的值为"a", "aa", "aaa", "aaaa", "aaaaa"
,那递归下去,第一次递归结束,是a
不匹配b
。然后再去循环判断ab
中的每个子串是否在dict中,然后再去判断aab
中每个子串是否在dict中。有没有发现什么,aab
中,ab
子串已经判断过了,递归的过程中又进行了一次重复判断。这样,这上面的方法的时间复杂度就是O(nn),时间复杂度超级高了。那既然有很多重复判断,我们何不用一个容器将以前判断的结果记录下来,后面判断的时候直接用呢。所以有了下面优化的版本:
import java.util.*; public class Solution { HashMap<String,Boolean> flag=new HashMap<>(); public boolean wordBreak(String s, Set<String> dict) { if (s.length()==0) return true; if (flag.containsKey(s)) return flag.get(s); for (int i=1;i<=s.length();++i){ String sub=s.substring(0,i); boolean b=dict.contains(sub); flag.put(sub,b); if (b&&wordBreak(s.substring(i),dict)) return true; } return false; } }
但是这种太占内存了,当字符串长度很长的时候,其子串会非常多,导致HashMap占用内存非常大。搬运LeetCode里面的解法,代码如下:
public boolean wordBreak(String s, Set<String> wordDict) { return word_Break(s, wordDict, 0, new Boolean[s.length()]); } public boolean word_Break(String s, Set<String> wordDict, int start, Boolean[] memo) { if (start == s.length()) { return true; } if (memo[start] != null) { return memo[start]; } for (int end = start + 1; end <= s.length(); end++) { if (wordDict.contains(s.substring(start, end)) && word_Break(s, wordDict, end, memo)) { return memo[start] = true; } } return memo[start] = false; }
方法2:动态规划
讨论里面很多都是用动态规划的方法做的,其基本原理是将一个字符串分成两个部分,如果两个部分都满足要求,则总的字符串是满足要求的,这两个部分又可以继续去判断。具体代码如下:
public boolean wordBreak(String s, Set<String> wordDict) { int len = s.length(); boolean[] flags = new boolean[len+1]; flags[0] = true; for (int i = 1; i <= len; i++) { for (int j = 0; j < i; j++) { if (flags[j] == true && wordDict.contains(s.substring(j,i))) { flags[i] = true; break; } } } return flags[len]; }
方法3:BFS
搬运自LeetCode官方题解,自己跑一遍应该能看懂。
public boolean wordBreak(String s, Set<String> dict) { Queue<Integer> queue = new LinkedList<>(); int[] visited = new int[s.length()]; queue.add(0); while (!queue.isEmpty()) { int start = queue.remove(); if (visited[start] == 0) { for (int end = start + 1; end <= s.length(); end++) { if (dict.contains(s.substring(start, end))) { queue.add(end); if (end == s.length()) { return true; } } } visited[start] = 1; } } return false; }
但上面的代码还有优化的空间。我们看到,红框部分,是从end到s.length遍历的,对应的子串长度为end-start
。这就导致,有一些不可能出现的子串被判断了。假设dict中,最长的单词为3个字母,那当(end-start)>3
时,就没必要再去判断了。
所以,我们再做一下优化,优化代码如下:
public boolean wordBreak(String s, Set<String> dict) { Queue<Integer> queue = new LinkedList<>(); int[] visited = new int[s.length()]; queue.add(0); //扫描一遍dict,记录最长的单词长度 int maxLen=-1; for (String item:dict){ if (item.length()>maxLen) maxLen=item.length(); } while (!queue.isEmpty()) { int start = queue.remove(); if (visited[start] == 0) { for (int end = start + 1; end <= s.length(); end++) { if ((end-start)<=maxLen&&dict.contains(s.substring(start, end))) { queue.add(end); if (end == s.length()) { return true; } } } visited[start] = 1; } } return false; }
经过测试,使用BFS的优化过的效率最高。