【问题标题】:Finding all combinations of words that form a word查找构成单词的所有单词组合
【发布时间】:2019-08-20 02:54:02
【问题描述】:

我有一个单词列表,有些单词可以用两个或多个其他单词组成,我必须返回所有这样的组合。

输入:

words = ["leetcode","leet","code","le","et","etcode","de","decode","deet"]

输出:

("leet","code") ("le","et","code") ("de","code") 等等..

我尝试了什么:

1) 尝试所有可能的组合会花费太多时间,而且是个坏主意。

2)我在这里感觉到某种形式的动态规划,就像我可以在“leetcode”中使用“leet”的解决方案。但我无法用伪代码准确地表述它。我该怎么做?

【问题讨论】:

    标签: string algorithm combinations dynamic-programming


    【解决方案1】:

    简单的方法:
    对单词列表进行排序。
    对于每个单词 A (leetcode),使用二进制搜索查找作为单词 A ('le', leet) 前缀的单词范围。
    对于每个有效前缀重复搜索单词的其余部分(即查找etcode 和code),依此类推

    【讨论】:

      【解决方案2】:

      你能每个词只用一次吗?例如,如果您有 de 和 dede,那么 (de, de) 是一个答案吗?为了简单起见,我只是假设每个单词出现一次,你有很多单词但没有记忆限制。

      1- 构建一个自定义树,使得每个节点如下所示:

      class node():
         is_there_a_word_that_ends_here = T/F
         children = dict() # other nodes, key is the letter of the node
      

      例如,如果您有三个单词,例如 ["ab", "abc", "ade", "c"],那么您将有一个如下所示的树(如果 is_there_a_word_that_ends_here 节点的值为 true,则我放一个 * 号)

      head ---a---b*---c*
          |   |
          |   L-d----e*
          |
          L--c*
      
      

      2-根据长度对单词进行分组。从长度最小的单词开始,因为当您接触到较大的单词时,您想知道较小单词的“细分”。在这里,您可以使用add_word_to_result 可以(应该)缓存结果的函数递归地执行此操作。

      results = dict() # keys: possible words you can reach, values: ways to reach them
      for word in words_in_ascending_length:
         add_word_to_result(word, tree, results)
      

      和add_word_to_result 将开始在树中移动。如果它在一个节点中看到is_there_a_word_that_ends_here,它会调用add_word_to_result(remaining_part_of_the_word, tree, results)。例如,如果您有“abc”,那么您会在“ab”中看到 *,然后调用 add_word_to_result("c", tree, results)。

      实现递归函数是问题的“有趣部分”(也是更耗时的部分),所以我把它留给你。此外,作为奖励,您可以想办法以一种有效的方式避免将重复项添加到结果中(因为在某些情况下会发生重复项)。

      (编辑:也许您需要缓存现有单词和不存在单词的细分 - 例如单词结尾的细分 - 以便在返回结果之前不必将它们分开,如果这句话使任何意义)

      我希望这会有所帮助。

      奖励:示例代码(还没有真正测试过,但应该可以工作,并且您可以做出重大改进,但我现在懒得做。您可以稍微更改结构以将 results 传递给 add_word_to_result , 这样您就可以记住迄今为止所有可能的组合,因此您只需使用它而不是 add_word_to_result(head, head, words_left[1:], combinations, words_passed+words_left[0]+","),而不要进行不必要的递归)

      words = ["leetcode", "leet", "code", "le", "et", "etcode", "de", "decode", "deet"]
      
      
      class node():
          def __init__(self, letter, is_there_a_word_that_ends_here):
              self.letter = letter # not really used but it feels weird to not have it in class
              self.is_there_a_word_that_ends_here = is_there_a_word_that_ends_here
              self.children = dict()
      
      
      # actually defining tree is redundant you can just merge tree and node class together, but maybe this is more explicit
      class Tree():
          def __init__(self):
              self.head = node(None, False)
      
          def add(self, word, head=None):
              if head is None:
                  head=self.head
      
              if word[0] not in head.children.keys():
                  head.children[word[0]] = node(word[0], False)
      
              if len(word) == 1:
                  head.children[word[0]].is_there_a_word_that_ends_here = True
              else:
                  self.add(word[1:], head=head.children[word[0]])
      
      
      words = sorted(words, key=lambda w: len(w))
      results = dict()
      tree = Tree()
      for word in words:
          tree.add(word)
      
      
      def add_word_to_result(head, current_node, words_left, combinations, words_passed):
          if words_left[0] in current_node.children.keys():
              # this does not have to happen because we call this function with words that are not in the list as well
              next_node = current_node.children[words_left[0]]
              if len(words_left) == 1 and next_node.is_there_a_word_that_ends_here:
                  combinations.append(words_passed+words_left)
              elif next_node.is_there_a_word_that_ends_here:
                  add_word_to_result(head, head, words_left[1:], combinations, words_passed+words_left[0]+",")
                  add_word_to_result(head, next_node, words_left[1:], combinations, words_passed + words_left[0])
              else:
                  add_word_to_result(head, next_node, words_left[1:], combinations, words_passed+words_left[0])
      
      
      for word in words:
          results[word] = []
          add_word_to_result(tree.head, tree.head, word, results[word], "")
      
      print(results)
      
      # {'le': ['le'], 'et': ['et'], 'de': ['de'], 'leet': ['le,et', 'leet'], 'code': ['code'], 'deet': ['de,et', 'deet'], 'etcode': ['et,code', 'etcode'], 'decode': ['de,code', 'decode'], 'leetcode': ['le,et,code', 'le,etcode', 'leet,code', 'leetcode']}
      
      

      【讨论】:

      • trie 的目的是什么?一套不是更好吗?它会查找 O(1) 时间。
      • 如果我们假设散列是瞬时的,是的。但我假设散列会使用每个单词中的每个字符。 Tree 也这样做,所以我看不出在这里使用 set 有什么显着优势,但我不是专家,所以如果有任何反对意见,我很乐意听到。我使用树的原因是,您可以轻松找到与您感兴趣的单词具有相同开头的单词。在集合中,您在相似词之间没有任何联系,因此您需要搜索,或者想出另一个索引。
      【解决方案3】:

      在递归中,找出基本情况很重要。由于您要求所有组合,看起来您需要返回一个二维数组。

      当我到达“”时,我应该返回什么?这是您在解决任何递归问题时应该问自己的主要问题。由于结构将是二维数组,所以当我点击“”时应该返回 [[]],并在返回“LEETCODE”时填充它。

      这是您必须实现的逻辑。从“Leetcode”中,我查看了所有给定的单词,发现“leet”是数组中存在的一个单词。您必须小心的一件事是,单词数组中给出的单词必须是前缀。例如,如果目标词是“eleetcode”,我们就不会使用“leet”,可能是“ele”或“eleet”。根据这些信息,您只需要实现编码。我会在python中:

      class Solution:
          def construct(self,target:str,words:list[str]):
              # we stored in result
              result=[]
              # Base case
              if target=='':
                  return [[]]
              # our tree will be branched out to len(words)
              for word in words:
                  # we look for a word that is prefix to our target word
                  
                  if target.startswith(word):
                      # if we found a word, then we are removing, and testing with the new target word
                      suffix=target[len(word):]
                      # we are recursively calling till the edge case
                      bubble_up_ways=self.construct(suffix,words)
                      # in each level I am adding the word as shown in the image
                      combinations=[[*way,word] for way in bubble_up_ways]
                      if combinations:
                          result.extend(combinations)
              return result
      

      但是,这是一个蛮力解决方案。这意味着我们将为给定数组中的每个单词调用递归调用。我们肯定会有 len(words)=n 分支。在基本情况之前,我们将进行递归调用。现在达到基本情况的最坏情况是什么。想象一下我们有我们的数组 ["l","e","e","t","c","o","d","e"]。所以为了达到基本情况,我们将有 len("leetcode")=m 递归调用。 m 也称为高度。所以我们将有“n over m”的递归调用。此外,对于每个递归调用,我们都在对数组combinations=[[*way,word] for way in bubble_up_ways] 进行切片,这也需要“m”时间。总而言之

        T:O(n^m * m) # this is exponential, because exponent is a variable
      

      对于空间复杂度,我们在堆栈上有“m”个递归调用,并且我们存储了可以是“n”长度的result 数组。因为每个分支都可能返回一个组合,所以我们必须存储“n”个数组。

        S: O(m*n)
      

      在递归函数中,如果我们在返回之前存储每个结果,而不是进行相同的计算,我们只需从存储中检索结果。这样,对于每个分支,我们不必在基本情况之前进行“m”个递归调用。这是记忆的版本,我们只是传递一个 memo={} 存储计算结果

      def memoized(self,target:str,words:List[str],memo={}):
          if target in memo:
              return memo[target]
          if target=="":
              return [[]]
          result=[]
          for word in words:
              if target.startswith(word):
                  suffix=target[len(word):]
                  buble_up_ways=self.memoized(suffix,words,memo)
                  combinations=[[*way,word] for way in buble_up_ways]
                  if combinations:
                      result.extend(combinations)
          # memorize before returning value
          memo[target] = result
          return result
      

      但是,与任何其他动态问题不同,这不会改变时间复杂度。因为如果我们知道会有重复值,我们就会存储这些值,但是在我们用数组中的一个单词修剪“leetcode”之后,我们将得到不同的结果。例如

      leetcode - leet ---> code # first branch
         leetcode - le   ---> etcode # second branch
      

      【讨论】:

        猜你喜欢
        • 2015-09-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-09-14
        • 1970-01-01
        相关资源
        最近更新 更多