题目描述
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
分析:第一感觉是用回溯法做,从字符串 s 开头开始,判断字典中的单词是否匹配 s ,用回溯法做,最终只要有一个能完全匹配,即可返回 True
但是这个处理过程, 有点难以下手,参考别人的做法后,用 dp 来做,不看别人思路一脸懵逼,看了恍然大悟,还是要多练多总结 dp 套路呀!
这个题属于 dp 中的字符串问题,
- 一般是 以字符串 第 i 个结尾的字符串的最优策略,比如本题可延伸为字符串相对于字典的最大匹配长度,
- 前 i 个字符串的最优策略,最后一个字符串可以用上,可以不用。
本题属于第一种情况: - dp[i]: s[0,1,i-1] 能否拆分,能为 true, 不能为 false. 通常这种情况,我们将 dp 数组长度设置为 s.lengh + 1 ,dp[0] = true. dp[i] 的状态取决于dp[0,j,i-1],如果有一个 dp[j] + word = s[:i], word 属于 wordDict,那么 dp[i] = true; dp[0] = true,可理解为不需要转换,就可拆分匹配。
解法二 : 回溯法
过程: 如 s = “leetcode” ,wordDict = [“leet”,“code”]
- “leetcode” 能否 break ,分解 为 “l" 是否在单词表中,和剩余的子串能否 break
- 回溯法考虑所有的可能情况,用指针 start 的位置代表各个位置的状态
- 如果指针的左侧的子串,是在单词表中,则只需要对以指针为开头的剩余子串进行递归掉用。
public class lc_139_wordBreak {
//方法一 :dp
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
if(n == 0 ) return true;
boolean [] dp = new boolean[n + 1];
HashSet<String> set = new HashSet<>(wordDict);
dp[0] = true;
for(int i = 1 ; i <= n ; i ++){
for(int j = 0 ; j < i ; j ++){
String tmp = s.substring(j,i);
if(dp[j] && set.contains(tmp)){
dp[i] = true;
break;
}
}
}
return dp[n];
}
}
解法二 : 未剪枝的回溯法,回溯法,未剪枝前,超时,比如案例 s = “aaaaaaaaaaaaaa…aab”,wordDict = [“a”,“ab”]; 每一个都会进入分支。
public class lc_139_wordBreak {
public boolean wordBreak1(String s, List<String> wordDict) {
int n = s.length();
if (n == 0) return true;
HashSet<String> set = new HashSet<>(wordDict);
int start = 0 ;
boolean res = dfs(s,set,start);
return res;
}
private boolean dfs(String s, HashSet<String> set, int start) {
//终止条件
if(start > s.length()-1){
return true;
}
for(int end = start + 1; end <= s.length(); end ++){
String word = s.substring(start,end);
if(set.contains(word) && dfs(s,set,end)){
return true;
}
}
return false;
}
}
增强版:记忆化回溯法,增加一个字典来记录已经得到的子树结果,遇到重复子树直接返回。
public class lc_139_wordBreak {
Map<String,Boolean> memo = new HashMap<>();
public boolean wordBreak2(String s, List<String> wordDict) {
int n = s.length();
if (n == 0) return true;
HashSet<String> set = new HashSet<>(wordDict);
return dfs1(set,s);
}
private boolean dfs1(HashSet<String> set, String s) {
if(s.equals("")) return true;
if(memo.containsKey(s)) return memo.get(s);
//遍历所有的分割位置
for(int i = 1; i < s.length() + 1; i++){
//获取一段字符串
String cur = s.substring(0,i);
//如果当前这个集合中没有这个单词,continue
if(!set.contains(cur)) continue;
//如果有,dfs
if(dfs1(set,s.substring(i))) {
memo.put(cur, true);
return true;
}
}
//如果此时以 s 开头不能分裂
memo.put(s,false);
return false;
}
}
总结:本题可作为回溯法 + 记忆化 一个很好的练习。
当然能想起来 dp ,首选 dp.