这道题好在哪里呢?
对我来说,这道题可以让我尝试5种解法:dp,dfs,dfs+ 备忘录,bfs,bfs+备忘录
所以这道题可以纳入精选里面。
题目链接
dp
使用dp是最容易让人理解的。
// 方法一:dp
private static boolean fun1(String s, List<String> dict) {
boolean[] dp = new boolean[s.length() + 1];
for (int i = 1; i <= s.length(); i ++) {
// 枚举k的值
for (int k = 0; k <= i; k ++) {
// 如果往前截取全部字符串,我们直接判断子串[0, i - 1]
// 是否存在于字典wordDict中即可
if (k == i) {
if (dict.contains(s.substring(0, i))) {
dp[i] = true;
continue;
}
}
// 递推公式
dp[i] = dp[i - k] && dict.contains(s.substring(i - k, i));
// 如果dp[i]为true,说明前i个字符结果拆解可以让他的所有字串都存在于字典wordDict中,直接终止内层循环,不用再计算dp[i]了
if (dp[i]) break;
}
}
return dp[s.length()];
}
- 它的逻辑思路是什么呢?
- 首先dp的原因是一事物现在的状态与过去的状态有关联
- 所以我们要从最开始标记状态,记录到一个数据结构中存储起来
- 有些情况,只用几个中间变量即可,这种是什么情况呢?
- 比如现在的状态只与前两个状态有关,那么就没有必要存那么多,反正也用不上
- 回到这个题
- 首先dp[0] 是false,因为这个表示的是一个空字符串,而字典wordDict中是没有空字符串的
- 所以从dp[1]开始即可
- 先看状态转移方程(这个是关键)
- dp[i] = dp[i - k] && dict.contains(s.substring(i - k, i));
- 以i为边界,将s分为两部分
- s的左边被k分为两部分
- 分别判断这两部分是否符合条件
- 对于k左边,用的是dp数组判断
- 对于k右边,用的是是否存在于字典中判断
- 原因在于dp数组存的是从下标0开始的,而不是从中间开始的(这句话只辅助理解)
- 需要注意的是:
- 在k等于i时,表示的是i之前作为一个子串看字典中有没有
- 还有一点就是:
- boolean[] dp = new boolean[s.length() + 1];
- 在开辟空间的时候,是len + 1,原因在于:0表示的是空,始终是false,对于字符串的第一个下标,就是1了,所以最后一个位置的下标就该是len,总长度自然是len + 1
做dp,最重要的是要抽象,不能着眼于细枝末梢,要脱离是关键。 - 还有两点,留给大家自己琢磨(还是那句话要有一定高度):
- 分别是以下两部分代码在代码里的作用,最关键的点在于怎么想问题才能把它加到逻辑里面:
if (dict.contains(s.substring(0, i))) {
dp[i] = true;
continue;
}
和
if (dp[i]) break;
上面的dp方法,有些地方都很有讲究,值得细细琢磨
dfs
// 方法二 dfs
class Func2 {
public boolean wordBreak(String s, List<String> wordDict) {
return dfs(s, wordDict, 0);
}
// start表示的是从字符串s的哪个位置开始
public boolean dfs(String s, List<String> wordDict, int start) {
// 字符串中的所有字符都遍历完了,就是到叶子节点了
// 说明字符串s可以拆分成在字典中出现的单词,直接返回true
if (start == s.length()) return true;
// 开始拆分字符串s
for (int i = start + 1; i <= s.length(); i ++) {
// 如果截取的子串不在字典中,继续截取更大的子串
if (!wordDict.contains(s.substring(start, i))) continue;
// 如果截取的子串在字典中,继续剩下的拆分
// 如果剩下的可以拆分成在字典中出现的单词,直接返回true
// 如果不能则继续截取更大的子串判断
if (dfs(s, wordDict, i)) return true;
}
return false;
}
}
- for (int i = start + 1; i <= s.length(); i ++)
- 这个式子不好一下子推出了,我是这样做的:
- 靠逻辑写了个for (int i = start; i < s.length(); i ++)
- 然后,调试,运行,就发现有漏洞没有考虑到,进行修补,就可以了。
- 那为什么i从start + 1开始呢?
- 因为要不然s.substring(start, i)就会是一个空字符串,所以这个函数其实是左闭右开的。
- 那为什么i <= s.length()呢,这不就超了吗
- 也是上面那个原因
dfs + 备忘录
public boolean wordBreak(String s, List<String> wordDict) {
return dfs(s, wordDict, new HashSet<>(), 0);
}
// start表示的是从字符串s的哪个位置开始
public boolean dfs(String s, List<String> wordDict, Set<Integer> indexSet, int start) {
// 字符串都拆分完了,返回true
if (start == s.length()) return true;
for (int i = start + 1; i <= s.length(); i ++) {
// 如果已经判断过,则直接跳过,防止重复判断
if (indexSet.contains(i)) continue;
// 截取子串,判断是否在字典中
if (wordDict.contains(s.substring(start, i))) {
if (dfs(s, wordDict, indexSet, i))
return true;
// 标记为已判断过
indexSet.add(i);
}
}
return false;
}
- 值得注意的是:
- 备忘录存的是下标
- 比如:存了个7,他的意思是:下标为7之后的子串是不符合题意的
- 以后7之前的子串即使有多种排列方式,一旦碰到7,就没必要继续比了,直接返回
- 因为后面的是不符合题意的
BFS
public boolean wordBreak(String s, List<String> wordDict) {
// 这里为了提高效率,把list转为set,因为set的查找效率要比list高
Set<String> setDict = new HashSet<>(wordDict);
// 记录当前层开始遍历字符串s的位置
Queue<Integer> queue = new LinkedList<>();
queue.add(0);
int length = s.length();
while (!queue.isEmpty()) {
int index = queue.poll();
// 如果字符串都遍历完了,自己返回true
if (index == length) return true;
for (int i = index + 1; i <= length; i ++) {
if (setDict.contains(s.substring(index, i)))
queue.add(i);
}
}
return false;
}
- 注意点是:
- 使用队列存储符合题意的下标,不断加进去,同时也不断推出
- 这个容易理解
bfs + 备忘录
public boolean wordBreak(String s, List<String> wordDict) {
// 这里为了提高效率,把list转化为set,因为set的查找效率比list高
Set<String> setDict = new HashSet<>(wordDict);
// 记录当前层开始遍历字符串s的位置
Queue<Integer> queue = new LinkedList<>();
queue.add(0);
int length = s.length();
// 记录访问过的位置,减少重复判断
boolean[] visited = new boolean[length];
while (!queue.isEmpty()) {
int index = queue.poll();
// 如果字符串都遍历完了,直接返回true
if (index == length) return true;
if (visited[index]) continue;
// 标记访问过
visited[index] = true;
for (int i = index + 1; i <= length; i ++) {
if (setDict.contains(s.substring(index, i)))
queue.add(i);
}
}
return false;
}
- 一定要自己写,否则不知道自己有多菜!多写
- 这里的备忘录同样存储的是:
- 例:存储的是7
- 意思是:
- 7后面的子串是不会符合题意的,把他存储起来,以后碰到7直接continue
- 这里的7在抽象逻辑上表示的不是下标7,而是以7开头到结尾的子串的意思。
- 更进一步解释就是:7后面的子串已经被纳入到计算中了,不用再次纳入一次
- 反正要求的是能或不能,并不是都是那些个
- 所以这里可以直接continue
- 我们会发现:
- 这些逻辑稍稍一变,就是一个新题型了
- 总结:
- 要掌握数据结构使用
- 抽象到逻辑层面,不要局限在细节里面,否则你钻不出来的
- 然后再修改你的逻辑细节
- 目前我的水平也就这个火候,还需多努力!
- 要时时回顾,过一阵子再回来看,就会比上一次的理解更深入一点