题目描述
给定一个字符串数组作为词典,再给定一个字符串。判断一下用词典中的词是不是可以组成这个字符串。
注意:词典中的词可以使用多次;词典中不存在重复的词
例如:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释:可以分解为leet code 两个词。
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释:可以分解为apple pen apple 三个词
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false
解释:没有匹配的词组成s
分析
暴力搜索
暴力搜索。例如输入s = “leetcode”。
从第0位开始找,substring(0,3)组成一个词,那接着判断剩余的能不能用词典组成。
时间复杂度最快情况下是O(
n
n
n^n
nn),就是每次向前走一步都是一个单词,而且还需要遍历到最后。例如s=“aaaaab”,wordDict=[“a”,“aa”,“aaa”,“aaaa”,“aaaaa”]
空间复杂度O(n)
public boolean wordBreak(String s, List<String> wordDict) {
return wordBreak(s,wordDict,0);
}
private boolean wordBreak(String s, List<String> wordDict,int start) {
if(start >= s.length()){
return true;
}
for(int i=start;i<=s.length();i++){
if(wordDict.contains(s.substring(start,i))){
if(wordBreak(s,wordDict,i)){
return true;
}
}
}
return false;
}
记忆化回溯
加缓存改进暴力搜索。如果已经被解决了的子问题,把结果保存下来。
private Boolean[] memory;
public boolean wordBreak(String s, List<String> wordDict) {
memory = new Boolean[s.length()+1];
return wordBreak(s,wordDict,0);
}
private boolean wordBreak(String s, List<String> wordDict, int start) {
if(start >= s.length()){
return true;
}
if(memory[start]!=null){
return memory[start];
}
boolean result = false;
for(int i=start;i<=s.length();i++){
if(wordDict.contains(s.substring(start,i))){
if(wordBreak(s,wordDict,i)){
result = true;
break;
}
}
}
memory[start] = result;
return memory[start];
}
动态规划
暴力搜索的逻辑是判断从0到i的子串可以被分解,继续判断从i+1到n的子串是不是能被分解。从整体来看。
例如"leetcode",wordBreak(0,8) 可以分解为wordBreak(0,4)+wordBreak(4,8),分解为2个子问题。
例如“catsanddog”,wordBreak(catsanddog) 可以分解为wordBreak(catsand)和wordBreak(dog)两个子问题,wordBreak(catsand) 又可以进一步分解为wordBreak(cat)和wordBreak(stand)两个子问题。
其实暴力搜索实现的就是这样的过程。
那和动态规划有什么不同呢?
我们假设 boolean[] dp = new boolean[n+1],dp[i]=true表示子串(0,i)能够被分解。基本条件是dp[0]=true,也就是说空字符串是可以被分解的。
动态转移方程:
d
p
[
i
]
=
t
r
u
e
,
当
且
仅
当
d
p
[
j
]
=
t
r
u
e
,
并
且
子
串
(
j
,
i
)
在
词
典
中
,
0
<
=
j
<
i
dp[i]=true,当且仅当dp[j]=true,并且子串(j,i)在词典中,0<=j<i
dp[i]=true,当且仅当dp[j]=true,并且子串(j,i)在词典中,0<=j<i
之前在想动态转移方程的时候会考虑dp[i]与dp[i-1]是什么关系。例如 LeetCode 62. Unique Paths 动态方程是: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − 1 ] dp[i][j]=dp[i−1][j]+dp[i][j−1] dp[i][j]=dp[i−1][j]+dp[i][j−1]
有时候也会找dp[i]和dp[i-1],dp[i-2]的关系,例如菲波那切数列类似的题目Leetcode 746. Min Cost Climbing Stairs动态方程是: d p [ i ] = m i n ( d p [ i − 1 ] + c o s t [ i − 1 ] , d p [ i − 2 ] c o s t [ i − 2 ] ] ) dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]cost[i-2]]) dp[i]=min(dp[i−1]+cost[i−1],dp[i−2]cost[i−2]])
而这里不确定具体是由哪个前面的状态转化过来的。具体是哪个子串关键是要符合两个条件,也有可能不能从任何一个前面的状态中转换过来。这应该是这个题目的难点,和不同之处。
在查找动态方程的时候,不要定式思维到dp[i-1],而是考虑从0到i-1,符合什么样的条件就可以转移到状态到i。有时候还要考虑是不是可以先获得dp[i+1],才能求得dp[i],例如查找最长的回文。
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
boolean[] dp = new boolean[n+1];
dp[0] = true;
for(int i=1;i<=n;i++){
for(int j = 0;j<i;j++){
if(dp[j] && wordDict.contains(s.substring(j,i))){
dp[i] = true;
break;
}
}
}
return dp[n];
}