问题

给定一个非空字符串 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

背包问题

从wordDict中选择若干元素(可重复),能否装满非空字符串s(背包)

单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。
拆分时可以重复使用字典中的单词,说明就是一个完全背包!

动规五部曲分析如下:

  • 确定dp数组以及下标的含义

    • **dp[i]** : 字符串长度为**i**的话,**dp[i]****true**,表示可以拆分为一个或多个在字典中出现的单词
  • 确定递推公式

    • 如果确定dp[j]true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。
    • 所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true
  • dp数组如何初始化

    • 从递归公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递归的根基,dp[0]一定要为true,否则递归下去后面都都是false
    • 那么dp[0]有没有意义呢?
      • dp[0]表示如果字符串为空的话,说明出现在字典里
    • 下标非0dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词
  • 确定遍历顺序

    • 题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包
    • 本题最终要求的是是否都出现过,所以对出现单词集合里的元素是组合还是排列,并不在意!

    • 但本题还有特殊性,因为是要求子串,最好是遍历背包放在外循环,将遍历物品放在内循环

    • 如果要是外层for循环遍历物品,内层for遍历背包,就需要把所有的子串都预先放在一个容器里。(如果不理解的话,可以自己尝试这么写一写就理解了)
  • 举例推导dp[i]

    • 以输入: s = “leetcode”, wordDict = [“leet”, “code”]为例,dp状态如图:

640 (1).webp

  1. public class Solution {
  2. public boolean wordBreak(String s, List<String> wordDict) {
  3. Set<String> wordDictSet = new HashSet(wordDict);
  4. boolean[] dp = new boolean[s.length() + 1];
  5. dp[0] = true;
  6. for (int i = 1; i <= s.length(); i++) { // 遍历背包
  7. for (int j = 0; j < i; j++) { // 遍历物品
  8. if (dp[j] && wordDictSet.contains(s.substring(j, i))) {
  9. dp[i] = true;
  10. break;
  11. }
  12. }
  13. }
  14. return dp[s.length()];
  15. }
  16. }
  • 时间复杂度:leetcode-139:单词拆分 - 图2,其中 n 为字符串 s 的长度。我们一共有 O(n) 个状态需要计算,每次计算需要枚举 O(n) 个分割点,哈希表判断一个字符串是否出现在给定的字符串列表需要 O(1) 的时间
  • 空间复杂度:leetcode-139:单词拆分 - 图3 ,其中 n 为字符串 s 的长度。我们需要 O(n) 的空间存放 dp 值以及哈希表亦需要 O(n)的空间复杂度