【问题标题】:Word Break algorithm分词算法
【发布时间】:2020-08-08 20:28:21
【问题描述】:

我正在尝试实现“分词”算法。

问题: 给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,确定 s 是否可以分割成一个或多个字典单词的空格分隔序列。

注意:

字典中的同一个词可能会在分词中重复使用多次。 您可以假设字典不包含重复的单词。

例子:

Input: s = "leetcode", wordDict = ["leet", "code"]
Output: true
Explanation: Return true because "leetcode" can be segmented as "leet code".

我的解决方案:

var wordBreak = function(s, wordDict) {
    if(!wordDict || wordDict.length === 0)
        return false;
    
    while(wordDict.length > 0 || s.length > 0) {
        const word = wordDict.shift();
        const index = s.indexOf(word);
        if(index === -1) {
            return false;
        }
        s = s.substring(0, index) + s.substring(index+word.length, s.length);
    }
    
    return s.length === 0 && wordDict.length === 0 ? true : false;
};

它适用于上面的示例(输入)。但是,下面的输入失败了。

Input: s = "applepenapple", wordDict = ["apple", "pen"]
Output: true
Explanation: Return true because "applepenapple" can be segmented as "apple pen apple".
             Note that you are allowed to reuse a dictionary word.

我怎样才能跟踪我已经删除的单词并在最后检查它。上面这个输入,剩下的 s 字符串包含字典中的“apple”,所以输出应该为真。

谢谢

【问题讨论】:

  • 是否允许在字典中有一些词是其他词的一部分,例如 ['part', 'partner']?
  • 你真的要使用shift吗?这会修改您的wordDict,使其无法重复使用。另外,您是在询问输入字符串是否仅包含 wordDict 中的子字符串,还是要在 wordDict 中的每个单词实例周围放置空格?换句话说,你对字符串 "leetecode" 有什么期望(在 leet 和 code 之间放置一个 "e")?

标签: javascript dynamic-programming


【解决方案1】:

这是我两年前在不同背景下遇到的一个有趣问题,即查询标记化。就我而言,字典中的单词数量大约为几百万,因此每次查找字典中不同单词的递归方法是不切实际的。此外,出于严格的效率原因,我需要应用动态规划来解决任务。

首先,我建议您使用AhoCorasick algorithm 在您的搜索字符串中查找单词。该算法在字符串长度内以线性时间在字符串中查找任意数量的模式,而不管要查找的模式数量如何(不再有单词数乘以字符串操作的长度,实际上每次查找一个单词string 需要扫描整个字符串..)。 幸运的是,我找到了算法 here 的 javascript 实现。

使用上面链接的代码和动态编程来跟踪出现在您的字符串中的单词,我编写了以下 javascript 解决方案:

function wordBreak(s, wordDict) {
    const len = s.length;
    const memoization_array_words = new Array(len).fill(null);
    const memoization_array_scores = new Array(len).fill(0);

    const wordScores = {};
    wordDict.forEach(function(word) {
        wordScores[word] = 1
    });

    automata = new AhoCorasick(wordDict);
    results = automata.search(s);

    results.forEach(function(result) {
        // result[0] contains the end position
        // result[1] contains the list of words ending in that position
        const end_pos = result[0];
        result[1].forEach(function(word) {
            const prev_end_pos = end_pos - word.length;
            const prev_score = (prev_end_pos == -1) ? 0 : memoization_array_scores[prev_end_pos];
            const score = prev_score + wordScores[word];
            if (score > memoization_array_scores[end_pos]) {
                memoization_array_words[end_pos] = word;
                memoization_array_scores[end_pos] = score;
            }
        });
    });

    if (memoization_array_words[len-1] == null) {
        return false;
    }
    solution = []
    var pos_to_keep = len - 1;
    while (pos_to_keep >= 0) {
        const word = memoization_array_words[pos_to_keep];
        solution.push(word);
        pos_to_keep -= word.length;
    }
    return solution.reverse()
}

其中memoization_array_wordsmemoization_array_scores 在我们遇到一个出现在前一个单词之后或字符串s 开头的单词时从左到右填充。代码应该是自动的,但如果你需要任何解释,请给我写评论。 另外,我为每个单词关联了一个分数(为简单起见,这里是 1),以便您区分不同的解决方案。例如,如果您将每个单词与一个重要性分数相关联,那么您最终会得到分数最高的标记化。在上面的代码中,词数最多的分词。

【讨论】:

    【解决方案2】:

    扩展版本: 如果有一个以测试字符串 (indexOf==0) 开头的单词,我会使用 some 测试 wordDict。如果是这样,我将字符串缩短为单词的长度,并使用缩短的字符串递归调用该函数。否则字符串不可拆分,我返回 false。我一直这样下去,直到发生错误或字符串的长度为 0,我赢了,因为一切正常。

    备注: WordBreak 与s= "cars" wordDict = ["car","ca","rs"] 现在已修复。为此,我以某种方法递归地调用算法。因此,如果一种方法在结束前停止,我会向后退并寻找替代方法,直到找到一种方法或没有任何可能。

    备注;数组.some
    在 array.forEach 中,如果不使用一些丑陋的技巧(例如 try...catch 并抛出错误),就无法使用 break,因此我可以使用 for 循环的经典变体。但是存在 array.some 方法,它像 forEach 循环一样循环,但只有一个元素要返回 true,因此结果为 true。

    示例:

    const array = [1, 2, 3, 4, 5];
    
    // checks whether an element is even
    const even = (element) => element % 2 === 0;
    
    console.log(array.some(even));

    这是工作算法的代码。

    var wordBreak = function(s, wordDict) {  
        if (!wordDict || wordDict.length === 0) return false;
        while (s.length > 0) {
            let test = wordDict.some( (word,index) => {
                if (s.indexOf(word)===0) {
                    s_new = s.substr(word.length);
                    return wordBreak(s_new, wordDict);
                }
            });
            if (!test ) return false;
            s=s_new;
        }
        if (s.length === 0) return true;    
    }
    
    s = "leetcode"; wordDict = ["leet", "code"];
    console.log(wordBreak(s, wordDict));
    
    s = "applepenapple"; wordDict = ["apple", "pen"];
    console.log(wordBreak(s, wordDict));
    
     s= "cars"; wordDict = ["car","ca","rs"];
     console.log(wordBreak(s, wordDict));

    【讨论】:

    • 你能解释一下一些方法吗?
    • 顺便说一句,输入 s= "cars" wordDict = ["car","ca","rs"] 失败。应该是真的。
    • 好吧,它变得越来越复杂,我必须寻找它。从你也想要这个的问题中并不清楚这一点。
    • 对不起,我应该解释得更好。我正在考虑用一个哈希表来存储字典中的单词,但我还不清楚
    • @myTest532 myTest532 我针对新情况扩展了代码并解释了一些方法。
    【解决方案3】:
    function wordBreak(dict, str){
      if (!str){
        return true;
      }
    
      for (const word of dict){
        if (str.startsWith(word)){
          return wordBreak(dict, str.substring(word.length, str.length))
        }
      }
    
      return false;
    }
    

    您也可以通过对数组进行预排序并使用二分搜索来优化 dict 循环,但希望这能说明问题。

    【讨论】:

    【解决方案4】:

    如果您正在寻找动态编程解决方案,我们将使用数组进行记录,然后我们将循环并跟踪单词。

    这将在 JavaScript 中传递:

    const wordBreak = function(s, wordDict) {
        const len = s.length
        const dp = new Array(len + 1).fill(false)
        dp[0] = true
        for (let i = 1; i < len + 1; i++) {
            for (let j = 0; j < i; j++) {
                if (dp[j] === true && wordDict.includes(s.slice(j, i))) {
                    dp[i] = true
                    break
                }
            }
        }
    
        return dp[s.length]
    }
    

    在 Python 中,我们会使用与字符串大小相同的列表(类似于 JavaScript 数组):

    class Solution:
        def wordBreak(self, s, words):
            dp = [False] * len(s)
            for i in range(len(s)):
                for word in words:
                    k = i - len(word)
                    if word == s[k + 1:i + 1] and (dp[k] or k == -1):
                        dp[i] = True
            return dp[-1]
    

    类似地,在 Java 中,我们会使用 boolean[]

    
    public final class Solution {
        public static final boolean wordBreak(
            String s,
            List<String> words
        ) {
            if (s == null || s.length() == 0) {
                return false;
            }
    
            final int len = s.length();
            boolean[] dp = new boolean[len];
    
            for (int i = 0; i < len; i++) {
                for (int j = 0; j <= i; j++) {
                    final String sub = s.substring(j, i + 1);
    
                    if (words.contains(sub) && (j == 0 || dp[j - 1])) {
                        dp[i] = true;
                        break;
                    }
                }
            }
    
            return dp[len - 1];
        }
    }
    

    这是 LeetCode 的 DP 解决方案:

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

    参考文献

    • 更多详细信息,请参阅Discussion Board,您可以在其中找到大量解释清楚且公认的解决方案,其中有多种languages,包括高效算法和渐近time/space 复杂性分析1,2.

    【讨论】:

    • 您能解释一下您的解决方案吗?
    • 我的意思是,你对问题的理解和方法,而不是 leetcode 解决方案或其他 leetcode 解决方案
    猜你喜欢
    • 2017-11-15
    • 1970-01-01
    • 2011-10-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多