动态规划完全背包问题07:单词拆分

119 阅读1分钟

单词拆分

力扣139. 单词拆分 - 力扣(LeetCode)
给你一个字符串 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
提示:单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。拆分时可以重复使⽤字典中的单词,说明就是⼀个完全背包!

动规五部曲:

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

boolea类型数组dp[i] : 字符串⻓度为i,dp[i]为true的话,表示可以拆分为⼀个或多个在字典中出现的单词。

2. 确定递推公式

字符串⻓度为i,在0 ~ i如果确定dp[j] 是true,且 [j, i] 这个区间的⼦串出现在字典⾥,那么dp[i]⼀定是true。(j < i )。所以递推公式是 if([j, i] 这个区间的⼦串出现在字典⾥ && dp[j]是true) 那么 dp[i] = true。

3. dp数组如何初始化

从递归公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递归的根基,dp[0]⼀定要为true,否则递归下去后⾯都都是false了。
那么dp[0]有没有意义呢?
dp[0]表示如果字符串为空的话,说明出现在字典⾥。但题⽬中说了“给定⼀个⾮空字符串 s” 所以测试数据中不会出现i为0的情况,那么dp[0]初始为true完全就是为了推导公式。下标⾮0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为⼀个或多个在字典中出现的单词。

4. 确定遍历顺序

题⽬中说是拆分为⼀个或多个在字典中出现的单词,所以这是完全背包。
还要讨论两层for循环的前后循序:
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
本题最终要求的是是否都出现过,所以对出现单词集合⾥的元素是组合还是排列,并不在意!那么本题使⽤求排列的⽅式,还是求组合的⽅式都可以。
即:外层for循环遍历物品,内层for遍历背包 或者 外层for遍历背包,内层for循环遍历物品 都是可以的。
但本题还有特殊性,因为是要求⼦串,最好是遍历背包放在外循环,将遍历物品放在内循环。 如果要是外层for循环遍历物品,内层for遍历背包,就需要把所有的⼦串都预先放在⼀个容器⾥。(如果不理解的话,可以⾃⼰尝试这么写⼀写就理解了)
所以最终我选择的遍历顺序为:遍历背包放在外循环,将遍历物品放在内循环。内循环从前到后。代码如下:

        for(int i = 0; i <= s.length(); i++)
            for(int j = 0; j < i; j++){
                if(dp[j] && list.contains(s.substring(j, i))){
                    dp[i] = true;
                    break;
                }
            }

5. 举例推导dp数组

image.png 分析完毕Java代码如下:

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        List<String> list = new ArrayList<>(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] && list.contains(s.substring(j, i))){
                    dp[i] = true;
                    break;
                }
            }
        return dp[s.length()];
    }
}

在提供一种更高效的代码:

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        int len = s.length(), maxw = 0;//maxw为最大物品(单词)长度
        boolean[] dp = new boolean[len + 1]; 
        dp[0] = true;
        Set<String> set = new HashSet();
        for(String str : wordDict){// 存储并找出最大单词长度
            set.add(str);
            maxw = Math.max(maxw, str.length());
        }
        for(int i = 1; i <= len; i++){//遍历背包
            for(int j = i; j >= 0 && j >= i - maxw; j--){//dp[i]只需要往前探索到词典里最长的单词即可
                if(dp[j] && set.contains(s.substring(j, i))){
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[len];
    }
}