如何用字典比蛮力更好地构建Plinko单词板?

时间:2019-01-19 20:37:19

标签: python algorithm

请考虑以下字母排列:

    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最熟悉,但至少我希望有一种可以解决该问题的算法或方法。谢谢。

2 个答案:

答案 0 :(得分:6)

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

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

我们可以使用如此严格的试探法来实施回溯搜索,似乎不太可能出现任何错误的路径。

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

什么是p(letter)?帕斯卡的三角形当然!根据Plinko板,n choose r恰好是此简单树的相关级别上所需字母的实例数。在第3层上,如果我们选择了RR,则需要3个N和3个E来表示该层上的“共享”字母。并且三个N中的每个必须具有相同的子字母(在这种情况下为W,X),并且三个E中的每个也必须具有(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

答案 1 :(得分:3)

我发现这是一个非常有趣的问题。

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

这足以在合理的时间内解决非显而易见的问题,例如以'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中的文件)