单词拆分 II - 动态规划

608 阅读4分钟

这是我参与更文挑战的第12天,活动详情查看: 更文挑战

肝了好多天-动态规划十连-超细腻解析|刷题打卡

这道题很经典啦,进阶必备😄😄😄 \color{green}{这道题很经典啦,进阶必备😄 😄 😄 ~}

什么题可以选择动态规划来做?

1.计数

  • 有多少种方式走到右下角
  • 有多少种方法选出k个数是的和是sum

2.求最大值最小值

  • 从左上角走到右下角路径的最大数字和
  • 最长上升子序列长度

3.求存在性

  • 取石子游戏,先手是否必胜
  • 能不能选出k个数使得和是sum

4.综合运用

  • 动态规划 + hash
  • 动态规划 + 递归
  • ...

leecode 140. 单词拆分 II

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,在字符串中增加空格来构建一个句子,使得句子中所有的单词都在词典中。返回所有这些可能的句子。

说明:

分隔时可以重复使用字典中的单词。

你可以假设字典中没有重复的单词。

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

输出:

[]


--

这是一道困难题,请先看单词拆分I

❤️❤️❤️❤️

2.1. 动态规划组成部分1:确定状态

简单的说,解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么,类似数学题中x, y, z代表什么

最后一步

我们定义 dp[i] 表示字符串 s 前 i 个字符组成的字符串 s[0..i−1] 是否能被空格拆分成若干个字典中出现的单词

假如在j这个位置进行空格拆分,那么判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词,我们可以判断s [0到j - 1] 和 s[j 到 i - 1] 这两部分是否在字典中

s[0到j-1] 我们可以用动态规划的思想,我存储之前的字典

例如,s = "leetcode", wordDict = ["lee",“t” ,"code"]

[0到j - 1] 可以指 ["lee",“t”] 这两个单词,因为dp[0] = true, d[1]在字典里,因此转移方程可以如下:

1.2. 动态规划组成部分2:转移方程

dp[i]=dp[j] && check(s[j..i−1])

这里的check可以是hash判断,截取的字符串是否在字典数组中。

1.3. 动态规划组成部分3:初始条件和边界情况

dp[0] = true; 空字符串是符合题意的。

1.4. 动态规划组成部分4:计算顺序

依次计算,用i去遍历0到j。

到这里,我们只是知道字符串在字典里,怎么组成句子,当然还得靠回溯+hash(记忆)😄😄😄 \color{green}{到这里,我们只是知道字符串在字典里,怎么组成句子,当然还得靠回溯 + hash(记忆)😄 😄 😄 ~}

参考代码

GO语言版

func wordBreak(s string, wordDict []string) (sentences []string) {
    wordSet := map[string]struct{}{}
    for _, w := range wordDict {
        wordSet[w] = struct{}{}
    }

    n := len(s)
    dp := make([][][]string, n)
    var backtrack func(index int) [][]string
    backtrack = func(index int) [][]string {
        if dp[index] != nil {
            return dp[index]
        }
        wordsList := [][]string{}
        for i := index + 1; i < n; i++ {
            word := s[index:i]
            if _, has := wordSet[word]; has {
                for _, nextWords := range backtrack(i) {
                    wordsList = append(wordsList, append([]string{word}, nextWords...))
                }
            }
        }
        word := s[index:]
        if _, has := wordSet[word]; has {
            wordsList = append(wordsList, []string{word})
        }
        dp[index] = wordsList
        return wordsList
    }
    for _, words := range backtrack(0) {
        sentences = append(sentences, strings.Join(words, " "))
    }
    return
}




dfs这个方法值得多看看。😄😄😄 \color{red}{dfs这个方法值得多看看。😄 😄 😄 ~}

java版

 public List<String> wordBreak2(String s, List<String> wordDict) {
        // 为了快速判断一个单词是否在单词集合中,需要将它们加入哈希表
        Set<String> wordSet = new HashSet<>(wordDict);
        int len = s.length();

        // 第 1 步:动态规划计算是否有解
        // dp[i] 表示「长度」为 i 的 s 前缀子串可以拆分成 wordDict 中的单词
        // 长度包括 0 ,因此状态数组的长度为 len + 1
        boolean[] dp = new boolean[len + 1];
        // 0 这个值需要被后面的状态值参考,如果一个单词正好在 wordDict 中,dp[0] 设置成 true 是合理的
        dp[0] = true;

        for (int right = 1; right <= len; right++) {
            // 如果单词集合中的单词长度都不长,从后向前遍历是更快的
            for (int left = right - 1; left >= 0; left--) {
                // substring 不截取 s[right],dp[left] 的结果不包含 s[left]
                if (wordSet.contains(s.substring(left, right)) && dp[left]) {
                    dp[right] = true;
                    // 这个 break 很重要,一旦得到 dp[right] = True ,不必再计算下去
                    break;
                }
            }
        }

        // 第 2 步:回溯算法搜索所有符合条件的解
        List<String> res = new ArrayList<>();
        if (dp[len]) {
            Deque<String> path = new ArrayDeque<>();
            dfs(s, len, wordSet, dp, path, res);
            return res;
        }
        return res;
    }

    /**
     * s[0:len) 如果可以拆分成 wordSet 中的单词,把递归求解的结果加入 res 中
     *
     * @param s
     * @param len     长度为 len 的 s 的前缀子串
     * @param wordSet 单词集合,已经加入哈希表
     * @param dp      预处理得到的 dp 数组
     * @param path    从叶子结点到根结点的路径, 既然是队列,先进后出,需要保存叶子结点到根结点的路径,那么先进的是根节点
     * @param res     保存所有结果的变量
     */
    private void dfs(String s, int len, Set<String> wordSet, boolean[] dp, Deque<String> path, List<String> res) {
        if (len == 0) { // 
            res.add(String.join(" ",path));
            return;
        }

        // 可以拆分的左边界从 len - 1 依次枚举到 0
        for (int i = len - 1; i >= 0; i--) {
            String suffix = s.substring(i, len);
            // 保证根节点或者叶子节点在集合中,dp[i] = true,表示并不会重复出现
            // 因此len = 0时,即可将结果添加
            if (wordSet.contains(suffix) && dp[i]) {  
                path.addFirst(suffix);  // 先进根节点,再进叶子节点
                dfs(s, i, wordSet, dp, path, res); // i--
                path.removeFirst(); // 先移除叶子节点,在移除根节点
            }
        }
    }

    @Test
    public void iswordBreak2() {
        ArrayList a = new ArrayList<String>();
        a.add("ab");
        a.add("c");
        a.add("a");
        a.add("bc");
        List<String> i = wordBreak2("abc", a);
        Assert.assertNotNull(i);
    }




❤️❤️❤️❤️

非常感谢人才们能看到这里,如果这个文章写得还不错,觉得有点东西的话 求点赞👍 求关注❤️ 求分享👥 对帅气欧巴的我来说真的 非常有用!!!

如果本篇博客有任何错误,请批评指教,不胜感激 !

文末福利,最近整理一份面试资料《Java面试通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。获取方式:GitHub github.com/Tingyu-Note…,更多内容关注公号:汀雨笔记,陆续奉上。