139: https://leetcode.com/problems/word-break/description/
140: https://leetcode.com/problems/word-break-ii/description/
139. Word Break
// 状态定义:boolean dp[i] 为 s.substring(0, i) 是否 breakable
// 状态转移方程:
// dp[i] = true if any index j < i, dp[j] is true && s.substring(j, i) in dictionary
// 初始化:boolean[] dp = new boolean[len + 1]; dp[0] = true
// 循环体
// 返回 target dp[len]
public boolean wordBreak(String s, List<String> wordDict) {
if (s == null || s.length() == 0) {
return false;
}
int len = s.length();
boolean[] breakable = new boolean[len + 1];
breakable[0] = true;
for (int i = 1; i <= len; i++) {
for (int j = 0; j < i; j++) {
if (breakable[j] && wordDict.contains(s.substring(j, i))) {
breakable[i] = true;
break;
}
}
}
return breakable[len];
}
此题目中,Dictionary
为 list,直接使用了 List.contains 来判断substring是否为 Dictionary 中的一个word,时间复杂度为 O(dictionary.size())。这有些类似 list.get(index) 的时间复杂度。LinkedList vs ArrayList. 这种问题不需要太较真,直接使用,复杂度计算时提出来就可以了,因为这并不是面试的重点。
实际工作中,这种问题内部使用时,应该直接声明 ArrayList 或 HashSet。避免不必要的开销。如果是 api,那就需要数据预处理了。
这道题目可以使用 Trie
来预处理构建 dictionary,该方法特别适合两种情况:1. dictionary 非常大; 2. 该function调用次数非常多。
这里还涉及到 string split
和 dp 对应string方式
中 index 的使用方式。这是 string 中常遇到且通用的问题。这里使用的 dp 对应 string 的方式如下:
String value | a | b | c | d | e | ||||||
---|---|---|---|---|---|---|---|---|---|---|---|
string index | 0 | 1 | 2 | 3 | 4 | ||||||
dp index | 0 | 1 | 2 | 3 | 4 | 5 | |||||
string split | 0 | 1 | 2 | 3 | 4 | 5 |
dp 对应 string 的方式同 string split 对应 string 的方式一致,对应 characters 中的间隙,而非 character 处。
-
dp[m]
为s.substring(0, m)
是否 breakable;s.substring(0, m)
值不包含characters.charAt(m)
; - 当计算
dp[n] (n > m)
时,对应为s.substring(m, n)
starting from indexm
目前理解,这种对应方式逻辑上简易清晰,且不容易出错误。
140. Word Break ii
这道题目要求列出所有 breakable 的组合。DP 并不能像139中带来优化,因为 139 中DP只是判断 index 处是否 breakable,而140 需要列出 index 处 breakable 的所有组合。直接 BackTracking:
public List<String> wordBreakII(String s, List<String> wordDict) {
List<String> res = new LinkedList<>();
// to pass timeout repeated test cases.139 word break.
if (!isBreakable(s, wordDict)) {
return res;
}
List<String> path = new LinkedList<>();
helper(s, wordDict, path, res);
return res;
}
private void helper(String str, List<String> dictionary, List<String> path, List<String> res) {
if (str.equals("")) {
res.add(String.join(" ", path)); // Java 8
return;
}
for (int i = 0; i < str.length(); i++) {
String sub = str.substring(0, i + 1);
if (dictionary.contains(sub)) {
path.add(sub);
helper(str.substring(i + 1), dictionary, path, res);
path.remove(path.size() - 1);
}
}
}
要在过程中记录下所有的合法结果,中间的操作会使得算法的复杂度不再是动态规划的两层循环,因为每次迭代中还需要不是constant的操作,最终复杂度会主要取决于结果的数量,而且还会占用大量的空间,因为不仅要保存最终结果,包括中间的合法结果也要一一保存,否则后面需要历史信息会取不到。
九章的Solution如下,思路很直观:
public class Solution {
public ArrayList<String> wordBreak(String s, Set<String> dict) {
// Note: The Solution object is instantiated only once and is reused by each test case.
Map<String, ArrayList<String>> memo = new HashMap<String, ArrayList<String>>();
return wordBreakHelper(s, dict, memo);
}
public ArrayList<String> wordBreakHelper(String s,
Set<String> dict,
Map<String, ArrayList<String>> memo){
if (memo.containsKey(s)) {
return memo.get(s);
}
ArrayList<String> results = new ArrayList<String>();
if (s.length() == 0) {
return results;
}
if (dict.contains(s)) {
results.add(s);
}
for (int len = 1; len < s.length(); ++len){
String word = s.substring(0, len);
if (!dict.contains(word)) {
continue;
}
String suffix = s.substring(len);
ArrayList<String> segmentations = wordBreakHelper(suffix, dict, memo);
for (String segmentation: segmentations){
results.add(word + " " + segmentation);
}
}
memo.put(s, results);
return results;
}
}
Trie implementation from programcreek(will implement my own version later on in Data Structures):
class TrieNode {
TrieNode[] arr;
boolean isEnd;
public TrieNode() {
this.arr = new TrieNode[26];
this.isEnd = false;
}
}
public class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode p = root;
for(int i=0; i<word.length(); i++){
char c = word.charAt(i);
int index = c-'a';
if(p.arr[index]==null){
TrieNode temp = new TrieNode();
p.arr[index]=temp;
p = temp;
}else{
p=p.arr[index];
}
}
p.isEnd=true;
}
// Returns if the word is in the trie.
public boolean search(String word) {
TrieNode p = searchNode(word);
if(p==null){
return false;
}else{
if(p.isEnd)
return true;
}
return false;
}
// Returns if there is any word in the trie
// that starts with the given prefix.
public boolean startsWith(String prefix) {
TrieNode p = searchNode(prefix);
if(p==null){
return false;
}else{
return true;
}
}
public TrieNode searchNode(String s){
TrieNode p = root;
for(int i=0; i<s.length(); i++){
char c= s.charAt(i);
int index = c-'a';
if(p.arr[index]!=null){
p = p.arr[index];
}else{
return null;
}
}
if(p==root)
return null;
return p;
}
}