找到由其他单词构成的最长单词

时间:2015-10-07 06:10:36

标签: java algorithm

我正在研究一个问题,即编写一个程序来查找单词列表中其他单词构成的最长单词。

EXAMPLE
Input: test, tester, testertest, testing, testingtester
Output: testingtester

我搜索并找到以下解决方案,我的问题是我在第2步中感到困惑,为什么我们应该以各种可能的方式打破每个单词?为什么不直接使用每个单词呢?如果有人能提供一些见解,那就太棒了。

以下解决方案执行以下操作:

  1. 按大小对数组进行排序,将最长的单词放在前面
  2. 对于每个单词,请以所有可能的方式将其拆分。也就是说,对于“test”,将其拆分为{“t”,“est”},{“te”,“st”}和{“tes”,“t”}。
  3. 然后,对于每个配对,检查前半部分和第二部分是否都存在于阵列的其他位置。
  4. 通过返回符合条件#3的第一个字符串“短路”。

4 个答案:

答案 0 :(得分:4)

间接回答您的问题,我相信以下是使用tries解决此问题的有效方法。

从字符串中的所有单词构建一个trie。

对单词进行排序,以便最长的单词出现。

现在,对于每个单词W,从trie的顶部开始,然后使用您正在测试的单词中的字母一次一个字母地在树下面开始跟随单词。

每当一个单词结束时,递归地从顶部重新输入trie,记下你已经“分支”。如果单词末尾的字母用完并且已经分支,那么你找到了一个复合词,因为单词是排序的,这是最长的复合词。

如果字母在任何时候停止匹配,或者你用完了并且不​​在单词的末尾,只需回溯到分支的任何地方并继续插入。

我担心我不太了解Java,所以我无法为您提供该语言的示例代码。但是,我已经在Python中编写了一个解决方案(使用来自this answer的trie实现)。希望你很清楚:

#!/usr/bin/env python3

#End of word symbol
_end = '_end_'

#Make a trie out of nested HashMap, UnorderedMap, dict structures
def MakeTrie(words):
  root = dict()
  for word in words:
    current_dict = root
    for letter in word:
      current_dict = current_dict.setdefault(letter, {})
    current_dict[_end] = _end
  return root

def LongestCompoundWord(original_trie, trie, word, level=0):
  first_letter = word[0]
  if not first_letter in trie:
    return False
  if len(word)==1 and _end in trie[first_letter]:
    return level>0
  if _end in trie[first_letter] and LongestCompoundWord(original_trie, original_trie, word[1:], level+1):
    return True
  return LongestCompoundWord(original_trie, trie[first_letter], word[1:], level)

#Words that were in your question
words = ['test','testing','tester','teste', 'testingtester', 'testingtestm', 'testtest','testingtest']

trie = MakeTrie(words)

#Sort words in order of decreasing length
words = sorted(words, key=lambda x: len(x), reverse=True)

for word in words:
  if LongestCompoundWord(trie,trie,word):
    print("Longest compound word was '{0:}'".format(word))
    break

考虑到上述情况,原始问题的答案会变得更加清晰:我们事先并不知道前缀词的组合会成功通过树。因此,我们需要准备好检查所有可能的前缀词组合。

由于您找到的算法无法有效地了解单词的哪些子集是前缀,因此它会在单词中的所有可能点处拆分该单词,以确保生成所有前缀。

答案 1 :(得分:2)

我猜你只是对哪些单词分裂感到困惑。

排序后,您可以通过减少长度来逐个考虑单词。让我们打电话给候选人"你试图分解的一个词。

如果候选人是由其他单词组成的,那么它肯定以一个单词开头,因此您将比较候选人的所有前缀与所有可能的单词。

在比较步骤中,您将候选前缀与整个单词进行比较,而不是分割单词。

顺便说一句,给定的解决方案不适用于三字及更长时间。修复方法如下:

  • 尝试候选人的每个前缀并将其与所有单词进行比较
  • 如果匹配,请使用后缀重复搜索。

示例:

window.open("example.html", "name", "resizable=1,scrollbars=no,width=500,height=200");提供前缀

testingtesterttetestesttestitestintestingtestingttestingtetestingtes

其中,testingtestetest是单词。然后,您需要尝试相应的后缀testingingtester

tester给出了

ingtesteriiningingtingteingtesingtest ,这些都不是文字。

ingteste是一个词,你已经完成了。

tester

答案 2 :(得分:2)

Richard's answer在很多情况下效果很好,但它可能需要指数时间:如果字符串W中有很多段,每个段都可以用多种不同的方式分解,就会发生这种情况。例如,假设W为abcabcabcd,其他字为abcabc。然后W的前3个字母可以分解为ab|ca|bc ...接下来的3个字母和接下来的3个字母也可以分解为2 ^ 3 = 8个可能的分解总共前9个字母:

a|bc|a|bc|a|bc
a|bc|a|bc|ab|c
a|bc|ab|c|a|bc
a|bc|ab|c|ab|c
ab|c|a|bc|a|bc
ab|c|a|bc|ab|c
ab|c|ab|c|a|bc
ab|c|ab|c|ab|c

所有这些部分分解最终都必然失败,因为输入中没有包含W的最终字母d的单词 - 但他的算法会在发现之前探索它们。通常,由abc后跟单d的n个副本组成的单词将花费O(n * 2 ^ n)时间。

我们可以通过记录有关W后缀的可分解性的额外信息(即后缀)来将此值提高到O(n ^ 2)最坏情况时间(以O(n)空间为代价)我们已经发现的W,我们可以或不能匹配单词序列。这种算法称为dynamic programming

我们需要某个单词W可被分解的条件正好是 W从其他单词集合中的某个单词X开始,和位于| X | +1开头的W的后缀是可分解 即可。 (我在这里使用的是基于1的索引,我将表示字符串S的子字符串,从位置i开始,到位置j由S [i..j]结束。)

每当我们发现在某个位置开始的当前单词W的后缀是或者不可分解时,我们可以记录这个事实并在以后使用它以节省时间。例如,在测试前面列出的8中的前4个分解之后,我们知道从位置4开始的W的后缀(即abcabcd可分解。然后,当我们尝试第5次分解时,即第一次以ab开始时,我们首先提出一个问题:W的其余部分,即从位置3开始的W的后缀,是否可以分解?我们还不知道,所以我们尝试添加c来获取ab|c,然后我们问:W的其余部分,即从第4位开始的W的后缀,是否可以分解?而且我们发现它已经被发现不是 - 所以我们可以立即得出结论,从ab|c开始也不可能分解W,而不是必须研究所有4种可能性。

假设当前字W是固定的,我们想要构建的是函数f(i),其确定从位置i开始的W的后缀是否可被分解。伪代码可能看起来像:

- Build a trie the same way as Richard's solution does.
- Initialise the array KnownDecomposable[] to |W| DUNNO values.

f(i):
    - If i == |W|+1 then return 1.  (The empty suffix means we're finished.)
    - If KnownDecomposable[i] is TRUE or FALSE, then immediately return it.
    - MAIN BODY BEGINS HERE
    - Walk through Richard's trie from the root, following characters in the
      suffix W[i..|W|].  Whenever we find a trie node at some depth j that
      marks the end of a word in the set:
        - Call f(i+j) to determine whether the rest of W can be decomposed.
        - If it can (i.e. if f(i+j) == 1):
            - Set KnownDecomposable[i] = TRUE.
            - Return TRUE.
    - If we make it to this point, then we have considered all other
      words that form a prefix of W[i..|W|], and found that none of
      them yield a suffix that can be decomposed.
    - Set KnownDecomposable[i] = FALSE.
    - Return FALSE.

调用f(1)然后告诉我们W是否可分解。

当调用f(i)返回时,KnownDecomposable [i]已设置为非DUNNO值(TRUE或FALSE)。只有当KnownDecomposable [i]为DUNNO时,才会运行该函数的主体。这些事实共同意味着函数的主体将只运行多次,因为可以调用函数。最多| W | +1这样的值,即O(n),并且在递归调用之外,对f(i)的调用最多花费O(n)时间来遍历Richard的trie,所以整体上是时间复杂性受O(n ^ 2)的限制。

答案 3 :(得分:1)

我可能会在这里使用递归。从最长的单词开始,找到它开头的单词。对于任何此类单词,将其从原始单词中删除,并以相同方式继续使用其余部分。

伪代码:

function iscomposed(orininalword, wordpart)
  for word in allwords
    if word <> orininalword
      if wordpart = word
        return yes
      elseif wordpart starts with word
        if iscomposed(orininalword, wordpart - word)
          return yes
        endif
      endif
    endif
  next
  return no
end  

main
  sort allwords by length descending
  for word in allwords
    if iscomposed(word, word) return word
  next
end

示例:

words:
abcdef
abcde
abc
cde
ab

通行证:

1. abcdef starts with abcde. rest = f. 2. no word f starts with found.
1. abcdef starts with abc. rest = def. 2. no word def starts with found.
1. abcdef starts with ab. rest = cdef. 2. cdef starts with cde. rest = f. 3. no word f starts with found.
1. abcde starts with abc. rest = cde. 2. cde itself found. abcde is a composed word