1. 问题描述:题目链接为:https://leetcode-cn.com/problems/word-break/
给定一个非空字符串 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
2. 思路分析:
① 分析题目可以知道我们可以使用一个for循环来截取对应的字符串,在给出的wordDict字符串是否包含当前截取的字符串,假如存在的话那么对于剩下来的字符串也是进行相同的操作,那么就可以使用递归进行求解,在一个for循环中截取对应的字符串判断字典中是否存在,如果存在那么进行剩余字符串的递归操作,在for循环中进行递归会尝试所有的字符串的可能情况,所以这也是一个多分支的递归搜索,所以其中存在很多的子问题的重复求解,比如是在for循环中进行多分支递归的时候那么就可能不同的分支但是对于同一个剩余字符串的重复求解判断,所以这个需要一个数据结构来记录求解出的中间结果,在递归往下求解的过程中先判断之前是否求解过假如有求解过那么直接返回结果不再递归往下求解
② 基于上面的递归过程中中间结果的记录,可以使用一个布尔类型的数组来记录,长度为字符串的长度,里面存着剩余字符串是否之前求解过的值,假如没有记录中间的求解过的结果的话那么时间复杂度会非常大,提交之后会超时,使用数据来记录的话则可以通过
③ 上面的递归求解的思路是比较容易理解的,官方的题解中除了上面的方法之外还可以使用宽度优先搜索来求解,宽度优先搜索使用队列,一开始队列假如字符串的开始下表,对字符串遍历进行截取操作,判断截取的字符串在给出的wordDict中是否存在假如存在那么将wordDict字典中包含的字符串的末尾下表加入到队列中,那么下一个队列开始的字符串的下表则从这个末尾的下表开始截取,在一个for循环中将所有以当前字母开头的字符串都加入到队列中,例如给出的测试用例中,catsandog中字典中包含的cats与cat都加入到队列中,这样的话判断所有可能的情况,这个在代码中也是比较好理解的,对于宽搜也是需要一个数据结构来记录,否则的话也会超时,这个记录与递归求解的记录也是差不多的
④ 还有一个方法是比较难想出来的,也就是动态规划,这个在官方的题解中能够可以看出来但是说实话比较难想出来,个人推荐推荐使用记忆化的递归求解,假如能够掌握动态规划也最好
记忆化递归代码如下:
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class Solution {
public static boolean wordBreak(String s, List<String> wordDict) {
return wordBreak(s, wordDict, 0, new Boolean[s.length()]);
}
private static boolean wordBreak(String s, List<String> wordDict, int start, Boolean[] rec) {
/*表明已经截取到了字符串的最末尾了这个时候说明已经单词可以进行拆分*/
if (start == s.length()) return true;
if (rec[start] != null) return rec[start];
for (int end = start + 1; end <= s.length(); ++end){
if (wordDict.contains(s.substring(start, end)) && wordBreak(s, wordDict, end, rec)){
return rec[start] = true;
}
}
return rec[start] = false;
}
}
宽度优先搜索代码如下:
import java.util.*;
public class Solution {
public static boolean wordBreak(String s, List<String> wordDict) {
Queue<Integer> queue = new LinkedList<>();
int vis[] = new int[s.length()];
/*搜索所有的可能的字符串前缀看一下*/
queue.add(0);
while (!queue.isEmpty()){
int start = queue.poll();
/*到达了s.length()说明已经包含了给出的单词可能的划分字符串了因为队列中判断出已经包含了所有的字符串这个时候已经到达尾部了*/
if (start == s.length()) return true;
/*记忆化的搜索*/
if (vis[start] == 0){
for (int end = start + 1; end <= s.length(); ++end){
if (wordDict.contains(s.substring(start, end))){
/*满足条件之后不能直接break因为可能当前的条件不满足需要搜索所有可能的条件*/
queue.add(end);
}
}
vis[start] = 1;
}
}
return false;
}
}
官方提供的动态规划代码:
public class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordDictSet=new HashSet(wordDict);
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for (int i = 1; i <= s.length(); i++) {
for (int j = 0; j < i; j++) {
if (dp[j] && wordDictSet.contains(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
}