• 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

    记忆化回溯:

    先把字典中的所有单词都存入 HashSet 中吧,这样我们就有了常数时间级的查找速度,我们得开始给字符串分段了,一个万能的做法是丢给递归函数,让其去递归求解,这里我们 suppose 递归函数会返回我们一个正确的值,如果返回的是 true 的话,表明我们现在分成的两段都在字典中,我们直接返回 true 即可,因为只要找出一种情况就行了。

    遍历了所有的情况,优点是写法简洁,思路清晰,缺点是存在大量的重复计算。

    所以我们需要进行优化,使用记忆数组 memo 来保存所有已经计算过的结果,再下次遇到的时候,直接从 cache 中取,而不是再次计算一遍。

    符串是否可以拆分,初始化为 -1,表示没有计算过,如果可以拆分,则赋值为1,反之为0。

    在之前讲的解法中,提到的是讲分成两段的后半段的调用递归函数,我们也可以不取出子字符串,而是用一个 start 变量,来标记分段的位置,这样递归函数中只需要从 start 的位置往后遍历即可,在递归函数更新记忆数组 memo 即可,参见代码如下:

    c++

    class Solution {
    public:
        bool wordBreak(string s, vector<string>& wordDict) {
            unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
            vector<int> memo(s.size(), -1);
            return check(s, wordSet, 0, memo);
        }
        
        bool check(string s, unordered_set<string>& wordSet, int start, vector<int>& memo) {
            if (start >= s.size()) return true;
            if (memo[start] != -1) return memo[start];
            
            for (int i = start + 1; i <= s.size(); ++i) {
                if (wordSet.count(s.substr(start, i-start)) && check(s, wordSet, i, memo)) {
                    return memo[start] = 1;
                }
            }
            return memo[start] = 0;
        }
    }; 
    

    java

    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;
        }
    }
    

    动态规划

    DP 解法的两大难点,定义 dp 数组跟找出状态转移方程,先来看 dp 数组的定义,这里我们就用一个一维的 dp 数组,其中 dp[i] 表示范围 [0, i) 内的子串是否可以拆分,

    注意这里 dp 数组的长度比s串的长度大1,是因为我们要 handle 空串的情况,我们初始化 dp[0] 为 true,然后开始遍历。

    注意这里我们需要两个 for 循环来遍历,因为此时已经没有递归函数了,所以我们必须要遍历所有的子串,

    我们用j把 [0, i) 范围内的子串分为了两部分,[0, j) 和 [j, i),其中范围 [0, j) 就是 dp[j],范围 [j, i) 就是 s.substr(j, i-j),其中 dp[j] 是之前的状态,我们已经算出来了,可以直接取,只需要在字典中查找 s.substr(j, i-j) 是否存在了,如果二者均为 true,将 dp[i] 赋为 true,并且 break 掉,此时就不需要再用j去分 [0, i) 范围了,因为 [0, i) 范围已经可以拆分了。最终我们返回 dp 数组的最后一个值,就是整个数组是否可以拆分的布尔值了,代码如下:

    c++

    class Solution {
    public:
        bool wordBreak(string s, vector<string>& wordDict) {
            unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
            vector<bool> dp(s.size() + 1);
            dp[0] = true;
            for (int i = 0; i < dp.size(); ++i) {
                for (int j = 0; j < i; ++j) {
                    if (dp[j] && wordSet.count(s.substr(j, i - j))) {
                        dp[i] = true;
                        break;
                    }
                }
            }
            return dp.back();
        }
    };
    

    java

    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()];
        }
    }
    

    python

    class Solution:
        def wordBreak(self, s: str, wordDict: List[str]) -> bool:
            import functools
            @functools.lru_cache(None)
            def back_track(s):
                if(not s):
                    return True
                res=False
                for i in range(1,len(s)+1):
                    if(s[:i] in wordDict):
                        res=back_track(s[i:]) or res
                return res
            return back_track(s)
    

    宽度优先搜索

    下面我们从题目中给的例子来分析:
    l 
    le e 
    lee ee e 
    leet 
    leetc eetc etc tc c 
    leetco eetco etco tco co o 
    leetcod eetcod etcod tcod cod od d 
    leetcode eetcode etcode tcode **code** 
    T F F F T F F F T 
    

    我们知道算法的核心思想是逐行扫描,每一行再逐个字符扫描,每次都在组合出一个新的字符串都要到字典里去找,如果有的话,则跳过此行,继续扫描下一行。

    BFS其实本质跟递归的解法没有太大的区别,递归解法在调用递归的时候,原先的状态被存入了栈中,这里 BFS 是存入了队列中,使用 visited 数组来标记已经算过的位置,作用跟 memo 数组一样,从队列中取出一个位置进行遍历,把可以拆分的新位置存入队列中,遍历完成后标记当前位置,然后再到队列中去取即可,参见代码如下:

    c++

    class Solution {
    public:
        bool wordBreak(string s, vector<string>& wordDict) {
            unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
            vector<bool> visited(s.size());
            queue<int> q{{0}};
            while (!q.empty()) {
                int start = q.front(); q.pop();
                if (!visited[start]) {
                    for (int i = start + 1; i <= s.size(); ++i) {
                        if (wordSet.count(s.substr(start, i - start))) {
                            q.push(i);
                            if (i == s.size()) return true;
                        }
                    }
                    visited[start] = true;
                }
            }
            return false;
        }
    };
    

    java

    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;
        }
    }
    

    python

    class Solution:
        def wordBreak(self, s: str, wordDict: List[str]) -> bool:       
            n=len(s)
            dp=[False]*(n+1)
            dp[0]=True
            for i in range(n):
                for j in range(i+1,n+1):
                    if(dp[i] and (s[i:j] in wordDict)):
                        dp[j]=True
            return dp[-1]
    
  • 相关阅读:
    Cookie-Session
    Chrome浏览器的Timing分析
    K-means: 多次random initialization来避免bad局部最优
    K-means: optimization objective(最小化cost function来求相应的参数)
    unsupervised learning: K-means 算法
    unsupervised learning: clustering介绍
    SVM: 实际中使用SVM的一些问题
    SVM: 使用kernels(核函数)的整个SVM算法过程
    SVM: 用kernels(核函数)来定义新的features,避免使用多项式,高斯kernel
    SVM:从数学上分析为什么优化cost function会产生大距离(margin)分类器
  • 原文地址:https://www.cnblogs.com/wwj99/p/12356341.html
Copyright © 2020-2023  润新知