Как построить словарную доску Плинко из словаря лучше, чем грубая сила?
Рассмотрим следующее расположение букв:
B
O A
N R I
D E N T
Начните с верхней буквы и выберите одну из двух букв ниже, в стиле Плинко, пока не дойдете до нижней. Независимо от того, какой путь вы выберете, вы создаете четырехбуквенное слово: BOND, BONE, BORE, BORN, BARE, BARN, BAIN или BAIT. Тот факт, что DENT читает через нижнюю часть, является просто хорошим совпадением.
Я хотел бы помочь выяснить алгоритм, который может спроектировать такой макет, где каждый возможный путь сверху вниз генерирует отдельное слово из (предоставленного) словаря. Входными данными для программы будут начальная буква (B, в этом примере) и длина слова n (4, в этом примере). Он будет возвращать либо буквы, которые составляют такой макет, либо сообщение о том, что это невозможно. Он не должен быть детерминированным, поэтому он может генерировать разные макеты с одним и тем же вводом.
До сих пор я не придумал ничего лучше, чем грубая сила. То есть для всех 26^[(n+2)(n-1)/2]
способов выбора букв для нижней части макета, чтобы проверить, дают ли все возможные 2^(n-1)
пути слова, которые в словаре. Я рассмотрел какое-то префиксное дерево, но тот факт, что пути могут пересекаться и обмениваться буквами, перепутал меня. Я чувствую себя наиболее комфортно в Python, но, как минимум, мне просто нравится идея алгоритма или подхода, который бы работал для этой проблемы. Благодарю.
Ответы
Ответ 1
Я нахожу это довольно интересной проблемой.
Первая попытка была случайным решателем; другими словами, он просто заполняет треугольник буквами и подсчитывает, сколько "ошибок" присутствует (слов нет в словаре). Затем выполняется восхождение на холм путем случайного изменения одной или нескольких букв и проверки улучшения ошибки; если ошибка остается той же самой, изменения все еще принимаются (таким образом, делая случайную прогулку по областям плато).
Удивительно, но это может решить в разумные сроки неочевидные проблемы, такие как 5-буквенные слова, начинающиеся с 'b':
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)
Другие шаги
В общем случае первый знак вопроса заменяется на все буквы в алфавите, если для всех слов, использующих выбранный знак вопроса, либо
- слово не имеет вопросительных знаков и находится в словаре, или
- в словаре есть слова, которые совместимы (все символы, кроме вопросительных знаков, совпадают)
Если не найдено решение для конкретного выбранного вопросительного знака, то нет смысла продолжать поиск, поэтому возвращается False
. Возможно, использование некоторой эвристики для выбора вопроса о том, какой знак вопроса будет заполнен первым, ускорит поиск, я не исследовал эту возможность.
Для случая 2 (поиск, если есть совместимые слова) я создаю 26*(N-1)
наборов слов, которые имеют определенный символ в определенной позиции (позиция 1 не рассматривается), и я использую пересечение множеств на всех символы без знака вопроса.
При таком подходе за 30 секунд (PyPy) можно определить, что нет решения для 5-буквенных слов, начинающихся с w
(в словаре 468 слов с этой начальной буквой).
Код для этой реализации можно увидеть на
https://gist.github.com/6502/26552858e93ce4d4ec3a8ef46100df79
(программа ожидает файл с именем words_alpha.txt
содержащий все допустимые слова, а затем должен вызываться с указанием начальной буквы и размера; в качестве словаря я использовал файл с https://github.com/dwyl/english-words)
Ответ 2
Притворись VWXYZ
внизу, здесь на самом деле полные слова.
B
A O
I R N
T N E D
V W X Y Z
Мы можем реализовать обратный поиск с помощью настолько строгой эвристики, что маловероятно, что какой-то неправильный путь зашел бы слишком далеко.
Вставьте все слова размером n
, начинающиеся с той же буквы, в простом дереве, как показано ниже. Теперь выполните поиск в глубину, утверждая следующее: каждому последующему уровню требуется одна дополнительная "общая" буква, что означает p(letter)
экземпляры этого уровня на этом уровне, с дополнительным требованием, чтобы их два потомка были одинаковыми буквами (например, два R
в скобках на уровне 2 могут быть "общими" буквами, потому что их дети одинаковы).
Что такое p(letter)
? Треугольник Паскаля конечно! n choose r
- это ровно количество экземпляров буквы, необходимых на соответствующем уровне этого простого дерева, согласно правлению Плинко. На уровне 3, если мы выбрали R
и R
, нам понадобятся 3 N
и 3 E
чтобы выразить "общие" буквы на этом уровне. И каждый из 3 N
должен иметь одинаковые дочерние буквы (в данном случае W, X), и каждый из 3 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
ОБНОВИТЬ
Из любопытства, вот код Python :)
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 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