【leetcode-139】【回溯超时、动态规划】单词拆分

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/word-break

【leetcode-55】跳跃游戏类似,回溯超时,转动态规划

我的解法(超时):

public class Solutiontemp {
    public boolean wordBreak(String s, List<String> wordDict) {
        for (int i=0;i<wordDict.size();i++) {
            if (match(s,wordDict.get(i))) {
                if (s.length() == wordDict.get(i).length()) {
                    return true;
                }
                if (wordBreak(s.substring(wordDict.get(i).length()),wordDict)){
                    return true;
                }
            }
        }
        return false;
    }
    public boolean match(String s,String word) {
        if (s.length() < word.length()) {
            return false;
        }
        for (int i=0;i<word.length();i++) {
            if (s.charAt(i) != word.charAt(i)) {
                return false;
            }
        }
        return true;
    }

leetcode官方解答:

方法 1:暴力

超时

最简单的实现方法是用递归和回溯。为了找到解,我们可以检查字典单词中每一个单词的可能前缀,如果在字典中出现过,那么去掉这个前缀后剩余部分回归调用。同时,如果某次函数调用中发现整个字符串都已经被拆分且在字典中出现过了,函数就返回 true 。

public class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        return word_Break(s, new HashSet(wordDict), 0);
    }
    public boolean word_Break(String s, Set<String> wordDict, int start) {
        if (start == s.length()) {
            return true;
        }
        for (int end = start + 1; end <= s.length(); end++) {
            if (wordDict.contains(s.substring(start, end)) && word_Break(s, wordDict, end)) {
                return true;
            }
        }
        return false;
    }
}

复杂度分析

时间复杂度:O(n^n)。考虑最坏情况 ss = \text{aaaaaaa}aaaaaaa 。每一个前缀都在字典中,此时回溯树的复杂度会达到 n^n。

空间复杂度:O(n)。回溯树的深度最深达到 n。

方法 2:记忆化回溯

在先前的方法中,我们看到许多函数调用都是冗余的,也就是我们会对相同的字符串调用多次回溯函数。为了避免这种情况,我们可以使用记忆化的方法,其中一个 memomemo 数组会被用来保存子问题的结果。每当访问到已经访问过的后缀串,直接用 memomemo 数组中的值返回而不需要继续调用函数。

通过记忆化,许多冗余的子问题可以极大被优化,回溯树得到了剪枝,因此极大减小了时间复杂度。

public class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        return word_Break(s, new HashSet(wordDict), 0, new Boolean[s.length()]);
    }
    public boolean word_Break(String s, Set<String> wordDict, int start, Boolean[] memo) {
        if (start == s.length()) {
            return true;
        }
        if (memo[start] != null) {
            return memo[start];
        }
        for (int end = start + 1; end <= s.length(); end++) {
            if (wordDict.contains(s.substring(start, end)) && word_Break(s, wordDict, end, memo)) {
                return memo[start] = true;
            }
        }
        return memo[start] = false;
    }
}

复杂度分析

时间复杂度:O(n^2) 。回溯树的大小最多达到 n^2 。

空间复杂度:O(n)。回溯树的深度可以达到 n级别。

方法 3:使用宽度优先搜索

另一个方法是使用宽度优先搜索。将字符串可视化成一棵树,每一个节点是用 endend 为结尾的前缀字符串。当两个节点之间的所有节点都对应了字典中一个有效字符串时,两个节点可以被连接。

为了形成这样的一棵树,我们从给定字符串的第一个字符开始(比方说 ss ),将它作为树的根部,开始找所有可行的以该字符为首字符的可行子串。进一步的,将每一个子字符串的结束字符的下标(比方说 ii)放在队列的尾部供宽搜后续使用。

每次我们从队列最前面弹出一个元素,并考虑字符串 s(i+1,end)s(i+1,end) 作为原始字符串,并将当前节点作为树的根。这个过程会一直重复,直到队列中没有元素。如果字符串最后的元素可以作为树的一个节点,这意味着初始字符串可以被拆分成多个给定字典中的子字符串。

public class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        Set<String> wordDictSet=new HashSet(wordDict);
        Queue<Integer> queue = new LinkedList<>();
        int[] visited = new int[s.length()];
        queue.add(0);
        while (!queue.isEmpty()) {
            int start = queue.remove();
            if (visited[start] == 0) {
                for (int end = start + 1; end <= s.length(); end++) {
                    if (wordDictSet.contains(s.substring(start, end))) {
                        queue.add(end);
                        if (end == s.length()) {
                            return true;
                        }
                    }
                }
                visited[start] = 1;
            }
        }
        return false;
    }
}

复杂度分析

时间复杂度:O(n^2)。对于每个开始的位置,搜索会直到给定字符串的尾部结束。

空间复杂度:O(n)。队列的大小最多 n。

方法 4:使用动态规划

动态规划思路:用一个数组res来存状态,0代表以当前索引对应的字符为结尾的字符串不可被完全拆分,1代表可以拆分。
之后遍历字符串的每个字符,对于每个字符,用一个指针p向前找距离在maxlen以内的字符串,当满足以下两个条件,即说明以当前字符结尾的字符串是可以被拆分的:

p指向的位置的状态是1(说明0到p的字符串是可以完全拆分的),且s[p+1:i]在字典里面
p指向的位置的状态是字符串的开头,且s[p+1:i]在字典里面

leetcode中文翻译得很晦涩:

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

原文地址:https://www.cnblogs.com/twoheads/p/11290296.html

时间: 2024-08-30 13:44:39

【leetcode-139】【回溯超时、动态规划】单词拆分的相关文章

leetcode 139 word break (单词拆分)

一开始的错误答案与错误思路,幻想直接遍历得出答案: 1 class Solution { 2 public: 3 bool wordBreak(string s, vector<string>& wordDict) { 4 for(int i;i<s.size();i++){ 5 int step=0; 6 for(int j;j<wordDict.size();j++){ 7 if(s.substr(i,wordDict[j].size())==wordDict[j]){

[LeetCode] 139. 单词拆分 ☆☆☆(动态规划 回溯)

139. 单词拆分 描述 给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词. 说明: 拆分时可以重复使用字典中的单词.你可以假设字典中没有重复的单词.示例 1: 输入: s = "leetcode", wordDict = ["leet", "code"]输出: true解释: 返回 true 因为 "leetcode" 可以被拆分成 "

【LeetCode-面试算法经典-Java实现】【139-Word Break(单词拆分)】

[139-Word Break(单词拆分)] [LeetCode-面试算法经典-Java实现][全部题目文件夹索引] 原题 Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separated sequence of one or more dictionary words. For example, given s = "leetcode", di

leetcode139.单词拆分

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词. 思路分析: (1)比较容易想到的解法是,从字符串 s 开头处开始匹配,若匹配到字典里的字符串,则继续匹配剩下的字符串.这样递归下去,直到找到一个可行的序列或尝试所有的单词组合. 提交的时候发现算法超时,后来想想其实这种解法存在很大的冗余,比如同时存在 a, b, ab的字典.只需记录下字符串 s 中的序列可以被字典匹配到即可,其实就是动态规划的思想: (2)采用动

01背包问题回溯法和动态规划

题目要求: 输入背包的容量v和物品的数量n:接下来n 行每行输入两个数字,第一个是物品质量,第二个是物品价值: 输出背包容纳物品的最大价值. 下面直接贴代码: 回溯法 1 #include<iostream>//之前必须知道背包容量和n个物品 2 #include<algorithm> 3 using namespace std; 4 class Property 5 { 6 public: 7 int weight,profit; 8 double average; 9 frie

[Leetcode] Backtracking回溯法解题思路

碎碎念: 最近终于开始刷middle的题了,对于我这个小渣渣确实有点难度,经常一两个小时写出一道题来.在开始写的几道题中,发现大神在discuss中用到回溯法(Backtracking)的概率明显增大.感觉如果要顺利的把题刷下去,必须先要把做的几道题题总结一下. 先放上参考的web: https://segmentfault.com/a/1190000006121957 http://summerisgreen.com/blog/2017-07-07-2017-07-07-算法技巧-backtr

单词拆分II

#单词拆分II#给一字串s和单词的字典dict,在字串中增加空格来构建一个句子,并且所有单词都来自字典.#返回所有有可能的句子.#EXAMPLE:#给一字串lintcode,字典为["de", "ding", "co", "code", "lint"]#结果为["lint code", "lint co de"]. class Solution:    "&

leetcode 139.单词拆分

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词. 说明: 拆分时可以重复使用字典中的单词. 你可以假设字典中没有重复的单词. 示例 1: 输入: s = "leetcode", wordDict = ["leet", "code"] 输出: true 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet cod

leetcode 单词拆分 II java

题目: 给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,在字符串中增加空格来构建一个句子,使得句子中所有的单词都在词典中.返回所有这些可能的句子. 说明: 分隔时可以重复使用字典中的单词. 你可以假设字典中没有重复的单词. 示例 1: 输入: s = "catsanddog" wordDict = ["cat", "cats", "and", "sand", "dog&qu