【问题标题】:How to build a Plinko board of words from a dictionary better than brute force?如何比蛮力更好地从字典中构建 Plinko 单词板?
【发布时间】:2019-06-13 17:36:56
【问题描述】:

考虑以下字母的排列方式:

    B
   O A
  N R I
 D E N T

从顶部字母开始,然后选择下面两个字母中的一个,Plinko 样式,直到到达底部。无论您选择哪种路径,您都会创建一个四个字母的单词:BOND、BONE、BORE、BORN、BARE、BARN、BAIN 或 BAIT。 DENT 读取底部的事实只是一个很好的巧合。

我想帮助找出一种可以设计这种布局的算法,其中从顶部到底部的每条可能的路径都会从(提供的)字典中生成一个不同的单词。程序的输入将是一个起始字母(本例中为 B)和一个字长 n(本例中为 4)。它会返回组成这样一个布局的字母,或者一条消息说这是不可能的。它不必是确定性的,因此它可能会使用相同的输入生成不同的布局。

到目前为止,我还没有想到比蛮力方法更好的方法。也就是说,对于为布局底部选择字母的所有26^[(n+2)(n-1)/2] 方式,检查是否所有可能的2^(n-1) 路径都给出了字典中的单词。我考虑过某种前缀树,但路径可以交叉和共享字母的事实让我很困惑。我在 Python 中最舒服,但至少我只是想要一个可以解决这个问题的算法或方法的想法。谢谢。

【问题讨论】:

  • BAIN 是什么?
  • 当然你还需要补充一点,单词必须是不同的,否则重复同一个单词会符合你的描述(例如F,II,NNN,EEEE:无论你选择单词的路径是“好")
  • @CJ59 方言词义(1)准备好,愿意,倾斜(2)短,接近。
  • 请描述您认为是“蛮力”的算法。
  • 顺便说一句,我等了几分钟,等待program by 6502 的回答,他为ENGLISH - 194,000 wordsb 5 编写了接受的分析器(764 个可接受的单词。有趣的是,它非常快地输出 b 4 的解决方案),直到我最终打断它。我下面的程序在 55 秒内输出一个解决方案。 (我使用的是顶级 MacBook Pro)

标签: python algorithm


【解决方案1】:

这里假设底部的V W X Y Z实际上是完整的单词。

    B
   A O
  I R N
 T N E D
V W X Y Z

我们可以用如此严格的启发式方法实现回溯搜索,似乎任何错误的路径都不太可能走得很远。

在下面的简单树中插入所有以相同字母开头的n 大小的单词。现在执行深度优先搜索,断言以下内容:每个连续级别都需要一个额外的“共享”字母,这意味着 p(letter) 在该级别上的实例,另外要求它们的两个孩子是相同的字母(例如,两个第 2 层括号中的 Rs 可能是“共享”字母,因为它们的孩子是相同的)。

p(letter) 是什么?当然是帕斯卡三角!根据 Plinko 板,n choose r 正是这个简单树的相关级别所需的字母实例数。在第 3 层,如果我们选择了 RR,我们将需要 3 个 Ns 和 3 个 Es 来表达该层的“共享”字母。并且 3 个Ns 中的每一个都必须具有相同的子字母(在这种情况下为 W,X),并且 3 个 Es 中的每一个也必须具有(X,Y)。

                     B
            /                 \
          A                     O
      /       \             /       \   
     I        (R)         (R)        N
    / \       / \         / \       / \
   T  (N)   (N)  E      (N)  E     E   D
  V W W X   W X X Y     W X X Y   X Y Y Z

4 W's, 6 X's, 4 Y's 

更新

出于好奇,这里有一些Python code :)

from itertools import combinations
from copy import deepcopy

# assumes words all start
# with the same letter and
# are of the same length
def insert(word, i, tree):
  if i == len(word):
    return
  if word[i] in tree:
    insert(word, i + 1, tree[word[i]])
  else:
    tree[word[i]] = {}
    insert(word, i + 1, tree[word[i]])

# Pascal's triangle
def get_next_needed(needed):
  next_needed = [[1, None, 0]] + [None] * (len(needed) - 1) + [[1, None, 0]]

  for i, _ in enumerate(needed):
    if i == len(needed) - 1:
      next_needed[i + 1] = [1, None, 0]
    else:
      next_needed[i + 1] = [needed[i][0] + needed[i+1][0], None, 0]
  return next_needed

def get_candidates(next_needed, chosen, parents):
  global log
  if log:
    print "get_candidates: parents: %s" % parents
  # For each chosen node we need two children.
  # The corners have only one shared node, while
  # the others in each group are identical AND
  # must have all have a pair of children identical
  # to the others' in the group. Additionally, the
  # share sequence matches at the ends of each group.
  #    I       (R)     (R)      N
  #   / \      / \     / \     / \
  #  T  (N)  (N)  E  (N)  E   E   D

  # Iterate over the parents, choosing
  # two nodes for each one
  def g(cs, s, seq, i, h):
    if log:
      print "cs, seq, s, i, h: %s, %s, %s, %s, %s" % (cs, s, seq, i, h)

    # Base case, we've achieved a candidate sequence
    if i == len(parents):
      return [(cs, s, seq)]
    # The left character in the corner is
    # arbitrary; the next one, shared.
    # Left corner:
    if i == 0:
      candidates = []
      for (l, r) in combinations(chosen[0].keys(), 2):
        _cs = deepcopy(cs)
        _cs[0] = [1, l, 1]
        _cs[1][1] = r
        _cs[1][2] = 1
        _s = s[:]
        _s.extend([chosen[0][l], chosen[0][r]])
        _h = deepcopy(h)
        # save the indexes in cs of the
        # nodes chosen for the parent 
        _h[parents[1]] = [1, 2]
        candidates.extend(g(_cs, _s, l+r, 1, _h))
        _cs = deepcopy(cs)
        _cs[0] = [1, r, 1]
        _cs[1][1] = l
        _cs[1][2] = 1
        _s = s[:]
        _s.extend([chosen[0][r], chosen[0][l]])
        _h = deepcopy(h)
        # save the indexes in cs of the
        # nodes chosen for the parent
        _h[parents[1]] = [1, 2]
        candidates.extend(g(_cs, _s, r+l, 1, _h))
      if log:
        print "returning candidates: %s" % candidates
      return candidates
    # The right character is arbitrary but the
    # character before it must match the previous one.
    if i == len(parents)-1:
      l = cs[len(cs)-2][1]
      if log:
        print "rightmost_char: %s" % l
      if len(chosen[i]) < 2 or (not l in chosen[i]):
        if log:
          print "match not found: len(chosen[i]) < 2 or (not l in chosen[i])"
        return []
      else:
        result = []
        for r in [x for x in chosen[i].keys() if x != l]:
          _cs = deepcopy(cs)
          _cs[len(cs)-2][2] = _cs[len(cs)-2][2] + 1
          _cs[len(cs)-1] = [1, r, 1]
          _s = s[:] + [chosen[i][l], chosen[i][r]]
          result.append((_cs, _s, seq + l + r))
        return result

    parent = parents[i]
    if log:
      print "get_candidates: g: parent, i: %s, %s" % (parent, i)
    _h = deepcopy(h)
    if not parent in _h:
      prev = _h[parents[i-1]]
      _h[parent] = [prev[0] + 1, prev[1] + 1]
    # parent left and right children
    pl, pr = _h[parent]
    if log:
      print "pl, pr: %s, %s" % (pl, pr)
    l = cs[pl][1]
    if log:
      print "rightmost_char: %s" % l
    if len(chosen[i]) < 2 or (not l in chosen[i]):
      if log:
        print "match not found: len(chosen[i]) < 2 or (not l in chosen[i])"
      return []
    else:
      # "Base case," parent nodes have been filled
      # so this is a duplicate character on the same
      # row, which needs a new assignment
      if cs[pl][0] == cs[pl][2] and cs[pr][0] == cs[pr][2]:
        if log:
          print "TODO"
        return []
      # Case 2, right child is not assigned
      if not cs[pr][1]:
        candidates = []
        for r in [x for x in chosen[i].keys() if x != l]:
          _cs = deepcopy(cs)
          _cs[pl][2] += 1
          _cs[pr][1] = r
          _cs[pr][2] = 1
          _s = s[:]
          _s.extend([chosen[i][l], chosen[i][r]])
          # save the indexes in cs of the
          # nodes chosen for the parent
          candidates.extend(g(_cs, _s, seq+l+r, i+1, _h))
        return candidates
      # Case 3, right child is already assigned
      elif cs[pr][1]:
        r = cs[pr][1]
        if not r in chosen[i]:
          if log:
            print "match not found: r ('%s') not in chosen[i]" % r
          return []
        else:
          _cs = deepcopy(cs)
          _cs[pl][2] += 1
          _cs[pr][2] += 1
          _s = s[:]
          _s.extend([chosen[i][l], chosen[i][r]])
          # save the indexes in cs of the
          # nodes chosen for the parent
          return g(_cs, _s, seq+l+r, i+1, _h)
    # Otherwise, fail 
    return []

  return g(next_needed, [], "", 0, {})

def f(words, n):
  global log
  tree = {}
  for w in words:
    insert(w, 0, tree)

  stack = []
  root = tree[words[0][0]]
  head = words[0][0]
  for (l, r) in combinations(root.keys(), 2):
    # (shared-chars-needed, chosen-nodes, board)
    stack.append(([[1, None, 0],[1, None, 0]], [root[l], root[r]], [head, l + r], [head, l + r]))

  while stack:
    needed, chosen, seqs, board = stack.pop()
    if log:
      print "chosen: %s" % chosen
      print "board: %s" % board
    # Return early for demonstration
    if len(board) == n:
      # [y for x in chosen for y in x[1]]
      return board

    next_needed = get_next_needed(needed)
    candidates = get_candidates(next_needed, chosen, seqs[-1])
    for cs, s, seq in candidates:
      if log:
        print "  cs: %s" % cs
        print "  s: %s" % s
        print "  seq: %s" % seq
      _board = board[:]
      _board.append("".join([x[1] for x in cs]))
      _seqs = seqs[:]
      _seqs.append(seq)
      stack.append((cs, s, _seqs, _board))

"""
    B
   A O
  I R N
 T N E D
Z Y X W V
"""
words = [
  "BONDV",
  "BONDW",
  "BONEW",
  "BONEX",
  "BOREW",
  "BOREX",
  "BAREW",
  "BAREX",
  "BORNX",
  "BORNY",
  "BARNX",
  "BARNY",
  "BAINX",
  "BAINY",
  "BAITY",
  "BAITZ"]
N = 5
log = True

import time
start_time = time.time()
solution = f(list(words), N)
print ""
print ""
print("--- %s seconds ---" % (time.time() - start_time))
print "solution: %s" % solution
print ""
if solution:
  for i, row in enumerate(solution):
    print " " * (N - 1 - i) + " ".join(row)
  print ""
print "words: %s" % words

【讨论】:

    【解决方案2】:

    我觉得这是一个非常有趣的问题。

    第一次尝试是随机求解器;换句话说,它只是用字母填充三角形,然后计算存在多少“错误”(字典中没有的单词)。然后通过随机更改一个或多个字母并查看错误是否改善来执行爬山;如果错误保持不变,则仍然接受更改(因此在高原地区进行随机游走)。

    令人惊讶的是,这可以在合理的时间内解决非显而易见的问题,例如以 'b' 开头的 5 个字母单词:

        b
       a u
      l n r
     l d g s
    o y s a e
    

    然后我尝试了一种完全搜索的方法来回答“无解”部分,我的想法是编写一个递归搜索:

    第一步

    只需在左侧写下所有可接受的单词即可;例如

        b
       a ?
      l ? ?
     l ? ? ?
    o ? ? ? ?
    

    并递归调用,直到找到可接受的解决方案或失败

    第 2 步

    如果第二个字母是,请在右侧写下所有可接受的单词 大于第一个单词的第二个字母,例如

        b
       a u
      l ? r
     l ? ? k
    o ? ? ? e
    

    这样做是为了避免搜索对称解决方案(对于任何给定的解决方案,只需在 X 轴上进行镜像即可获得另一个解决方案)

    其他步骤

    在一般情况下,如果对于所有使用所选问号的单词,第一个问号将替换为字母表中的所有字母

    1. 该词没有问号并且在字典中,或者
    2. 字典中有兼容的单词(除问号外的所有字符都匹配)

    如果没有找到所选特定问号的解决方案,则继续搜索没有意义,因此返回 False。可能使用一些启发式方法来选择首先填充哪个问号会加快搜索速度,我没有调查这种可能性。

    对于案例 2(搜索是否有兼容的单词)我正在创建 26*(N-1) 在某个位置(不考虑位置 1)具有规定字符的单词集,并且我在所有非-问号字符。

    这种方法能够在大约 30 秒 (PyPy) 内判断以 w 开头的 5 个字母单词没有解决方案(字典中有 468 个单词以该开头字母开头)。

    此实现的代码可见于

    https://gist.github.com/6502/26552858e93ce4d4ec3a8ef46100df79

    (程序需要一个名为words_alpha.txt 的文件,其中包含所有有效单词,然后必须指定首字母和大小来调用该文件; 作为字典,我使用了来自https://github.com/dwyl/english-words的文件)

    【讨论】:

    • @גלעדברקן 我没有看到您的解决方案,但即使在阅读后我也不明白您的方法。假设您的长度为 3,并且以 A 开头的单词列表是 ABD、ABE、ACG、ACF。什么是“完全二叉树”?用这些话没有解决方案,但是如果将ACG替换为ACE(解决方案是A/BC/DEF)就有解决方案...... trie 结构是相同的。中间的字母 E 被两个词使用(“交叉和分享”问题);如果您将单词 ABE 放在图表的左侧,则没有解决方案......但另一个有效的解决方案是 A/CB/FED(对称解决方案)。
    • 感谢您的澄清。我误解了这个问题。现在我看到了任何给定级别的共享信件问题。我会再考虑一下。
    • 感谢您分享您的方法背后的想法以及一些工作代码。这将为我自己尝试一些事情提供一个很好的起点。
    • 您能否提供一个包含您在测试中使用的 468 个单词的文件的链接 (words_alpha.txt)?我想用我的代码试试 :)
    • @גלעדברקן 我添加了对所用字典的引用
    猜你喜欢
    • 2014-02-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-08-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多