leetcode
leetcode 101 ~ 150
单词拆分

单词拆分

难度:

标签:

题目描述

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

 

示例 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

 

提示:

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • swordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串 互不相同

代码结果

运行时间: 17 ms, 内存: 16.0 MB


/*
 * 思路:
 * 仍然使用动态规划的方法,但是这次我们使用 Java Stream 来实现。
 * 定义一个布尔数组 dp,其中 dp[i] 表示字符串 s 的前 i 个字符是否可以由 wordDict 中的单词拼接成。
 * 初始化 dp[0] = true,因为空字符串可以由空字符串组成。
 * 使用 IntStream 来遍历字符串 s 的每一个位置 i,对于每一个位置 j,如果 dp[j] 为 true 并且 s.substring(j, i) 在 wordDict 中,那么 dp[i] = true。
 * 最终 dp[s.length()] 就是答案。
 */
 
import java.util.*;
import java.util.stream.IntStream;
 
public class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        Set<String> wordSet = new HashSet<>(wordDict);
        boolean[] dp = new boolean[s.length() + 1];
        dp[0] = true;
        IntStream.rangeClosed(1, s.length()).forEach(i -> {
            IntStream.range(0, i).filter(j -> dp[j] && wordSet.contains(s.substring(j, i))).findFirst().ifPresent(j -> dp[i] = true);
        });
        return dp[s.length()];
    }
}
 

解释

方法:

这个题解使用动态规划来解决单词拆分问题。它将问题转化为一个完全背包问题,其中背包的容量为字符串s的长度,物品为字典wordDict中的单词。dp[j]表示物品能否达成容量为j的背包。状态转移方程为:dp[j] = dp[j] or (dp[j - len(wordDict[i])] and s[j - len(wordDict[i]) : j] == wordDict[i]),即如果dp[j - len(wordDict[i])]为True,且s[j - len(wordDict[i]) : j]等于wordDict[i],则dp[j]为True。最后返回dp[len(s)]即可得到结果。

时间复杂度:

O(len(s) * len(wordDict))

空间复杂度:

O(len(s))

代码细节讲解

🦆
动态规划中的状态转移方程 `dp[j] = dp[j] or (dp[j - len(wordDict[i])] and s[j - len(wordDict[i]) : j] == wordDict[i])` 是如何确保不会重复使用同一位置的字符?
在状态转移方程中,每次考虑的是从位置 `j - len(wordDict[i])` 到 `j` 的子字符串是否与 `wordDict[i]` 相匹配,且前一个状态 `dp[j - len(wordDict[i])]` 必须为真,即以 `j - len(wordDict[i])` 为结尾的字符串可以被 `wordDict` 中的其他单词分割。这种方法每次转移是基于之前已验证的子字符串,所以不会重复使用同一位置的字符,每个字符位置只被用来验证一次是否能形成有效的单词分割。
🦆
在动态规划的实现中,对于 `dp` 数组的初始化,为什么只有 `dp[0]` 被设置为 `True`,而其他位置初始化为 `False`?
`dp[0]` 被设置为 `True` 是因为一个长度为0的字符串(空字符串)自然无需分割就是一个有效的分割。其他的 `dp[j]` 初始化为 `False` 表示在开始时,假设所有长度的字符串都不能被 `wordDict` 中的单词完全分割,这是一个安全的默认假设,只有当找到有效的单词匹配组合时才将对应的 `dp[j]` 更新为 `True`。
🦆
题解中提到这是一个完全背包问题,如何解释字符串 `s` 和 `wordDict` 之间的关系与传统的完全背包问题中的物品和容量之间的关系?
在传统的完全背包问题中,我们有一系列物品,每个物品可以被无限次选用,目标是填满一个给定的容量背包。在单词拆分问题中,字符串 `s` 的长度相当于背包的容量,而 `wordDict` 中的每个单词相当于背包问题中的一个物品。单词可以无限次使用来填充这个背包,即填充字符串 `s`。目标是完全使用 `wordDict` 中的单词填满整个字符串 `s` 的长度,不留空隙。
🦆
为什么题解建议外层循环遍历背包容量,内层循环遍历物品,这种遍历顺序对问题解决有什么特别的帮助?
这种遍历顺序有助于确保在更新 `dp[j]` 值时,所有可能的单词长度和对应的子问题(即更小的 `j` 值)已经被考虑。这样可以保证在尝试填充任何长度为 `j` 的背包时,我们已经考虑了所有可能的单词组合来达到这个长度。此外,这种方法避免了在更新 `dp[j]` 时重复计算或遗漏任何单词的可能性,使得动态规划的表填充更为有效和系统。

相关问题

单词拆分 II

给定一个字符串 s 和一个字符串字典 wordDict ,在字符串 s 中增加空格来构建一个句子,使得句子中所有的单词都在词典中。以任意顺序 返回所有这些可能的句子。

注意:词典中的同一个单词可能在分段中被重复使用多次。

 

示例 1:

输入:s = "catsanddog", wordDict = ["cat","cats","and","sand","dog"]
输出:["cats and dog","cat sand dog"]

示例 2:

输入:s = "pineapplepenapple", wordDict = ["apple","pen","applepen","pine","pineapple"]
输出:["pine apple pen apple","pineapple pen apple","pine applepen apple"]
解释: 注意你可以重复使用字典中的单词。

示例 3:

输入:s = "catsandog", wordDict = ["cats","dog","sand","and","cat"]
输出:[]

 

提示:

  • 1 <= s.length <= 20
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 10
  • s 和 wordDict[i] 仅有小写英文字母组成
  • wordDict 中所有字符串都 不同