题目:单词拆分
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,
判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
注意你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
package leetCode.DFS;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
public class lc_dfs_139_wordBreak {
/*
思路:
1)dfs,单纯的dfs会超时,要记忆化剪枝
2)dp
*/
public static void main(String[] args) {
String s = "leetcode";
List<String> list = Arrays.asList("leet", "code");
lc_dfs_139_wordBreak m = new lc_dfs_139_wordBreak();
System.out.println(m.wordBreak(s, list));
}
/*
dp:
1.状态:
1)最后一步
字符串拆分为(0,len-{x1,x2,x3...})和(len-{x1,x2,x3...},len)两部分,其中xi为wordDict的单词长度
2)子问题
原问题:(0,len)的字符串,是否可以被拆分
转化成(0,len-{x1,x2,x3...})的字符串,是否可以被拆分
所以dp[i]表示,以index=i结尾的字符串,是否可以被拆分
调整:
dp[i] 表示s中以i-1结尾的字符串是否可被 wordDict 拆分,可以理解为长度为i的子串是否可被拆分
如果dp[i] 表示s中以i结尾的字符串是否可被 wordDict 拆分,则dp[0]要单独判断,
且第二层for时,j总是从0开始,如果一旦dp[0]=false,则后面全部为false,则没办法状态转移
所以dp[0]要当做整个子串作为整体去匹配字典单词的边界
考虑第1个字符的情况,如果wordDictSet.contains(s[0])== true,而dp[0]==false将没办法进行dp状态转移
2.转移方程
dp[i]=dp[i-x1]\\dp[i-x2]\\dp[i-xj],xj为wordDict的单词长度
写成dp[i]=dp[j] && wordDict.contains(str.substring(j,i) )
3.初始条件和边界条件
dp[0]=true,很重要,j<i
*/
public boolean wordBreak(String s, List<String> wordDict) {
if (wordDict == null || wordDict.size() == 0)
return false;
int n = s.length();
// dp[i] 表示s中以i-1结尾的字符串是否可被 wordDict 拆分,可以理解为长度为i的子串是否可被拆分
// 如果dp[i] 表示s中以i结尾的字符串是否可被 wordDict 拆分,则dp[0]要单独判断,
// 且第二层for时,j总是从0开始,如果一旦dp[0]=false,则后面全部为false,则没办法状态转移
// 所以dp[0]要当做整个子串作为整体去匹配字典单词的边界
// 考虑第1个字符的情况,如果wordDictSet.contains(s[0])== true,而dp[0]==false将没办法进行dp状态转移
boolean[] dp = new boolean[n + 1];
dp[0] = true;//很重要
for (int i = 1; i < dp.length; i++) {
for (int j = 0; j < i; j++) {
// j-1结尾的为true,且下标为(j,i-1)的子字符串包含在字典中
if (dp[j] && wordDict.contains(s.substring(j, i))) {
dp[i] = true;//以i-1结尾的字符串可被 wordDict 拆分
break;
}
}
}
return dp[n];
}
public boolean wordBreak2(String s, List<String> wordDict) {
if (wordDict == null || wordDict.size() == 0)
return false;
return dfs(s, wordDict, 0, new HashSet<>());
}
/**dfs:
* 如果不记忆化dfs,会超时
* 里面会有很多重复的计算。如可以通过多种方式由index=0 -> index=60,
* 第一次计算得index=60开始不能完全拆分,那么后面若再次得到index=60就不用再次计算,直接false
*
* @param s
* @param wordDict
* @param index 当前index位置开始拆分的字符串索引,为基本变量,无需回溯回上一层的index
* @param set 用于记录处理过的、从index开始拆分的字符串,元素表示不能拆分的起始index
* @return
*/
public boolean dfs(String s, List<String> wordDict, int index, HashSet<Integer> set) {
if (index == s.length())//若最后字符串索引为最后一个位置之后,拆分成功
return true;
if (set.contains(index))//如果之前计算过从该索引开始拆分,则不能拆分,false
return false;
// 枚举字典里每一个单词,与s匹配
for (String word : wordDict) {
int endIndex = index + word.length();
if (s.startsWith(word, index)) {//s.startsWith():index开始是否以word为前缀
if (dfs(s, wordDict, endIndex, set)) {
return true;
}
}
}
// 到这里则表示拆分不成功,就将当前拆分不成功的index记录下,避免重复计算
if (!set.contains(index))
set.add(index);
// 这里index为基本变量,无需回溯回上一层的index
return false;
}
/*
和dfs思路一样,只是将匹配后的word从s里减去,set里记录的是s的当前长度,
元素表示该长度不能拆分成功
*/
public boolean dfs2(String s, List<String> wordDict, HashSet<Integer> set) {
if (s.equals(""))//若最后字符串没了,拆分成功
return true;
if (set.contains(s.length()))//如果之前计算过该长度,表示不能拆分,false
return false;
// 枚举字典里每一个单词,与s匹配
for (String word : wordDict) {
// 注意边界条件,字符串长度>=单词长度
if (s.length() >= word.length() && word.equals(s.substring(0, word.length()))) {
// 如果可以匹配,就将s减去匹配的部分,继续拆分匹配
if (dfs2(s.substring(word.length()), wordDict, set)) {//一旦成功直接返回true
return true;
}
}
}
// 如果最后拆分不成功,就将当前拆分不成功的子字符串长度记录下,避免重复计算
if (!set.contains(s.length()))
set.add(s.length());
return false;
}
}