Ответы:
Я придумал решение, которое, вероятно, не самое эффективное, но работает достаточно хорошо. В принципе:
Это рабочий, но зачастую довольно плохой кроссворд. Я внес ряд изменений в основной рецепт, приведенный выше, чтобы добиться лучшего результата.
Я недавно написал свой собственный на Python. Вы можете найти его здесь: http://bryanhelmig.com/python-crossword-puzzle-generator/ . Он создает не сложные кроссворды в стиле NYT, а стиль кроссвордов, который вы можете найти в детской книжке-головоломке.
В отличие от нескольких алгоритмов, которые я обнаружил там, которые реализовали случайный метод грубой силы для размещения слов, как предлагали некоторые, я попытался реализовать немного более умный подход грубой силы при размещении слов. Вот мой процесс:
В конце концов, у вас есть приличный кроссворд или головоломка для поиска слов, поскольку они примерно одинаковы. Как правило, он работает довольно хорошо, но дайте мне знать, если у вас есть предложения по улучшению. Более крупные сети работают экспоненциально медленнее; линейно большие списки слов. Списки слов большего размера также имеют гораздо больше шансов на лучшее размещение слов.
array.sort(key=f)
является стабильным, что означает (например), что простая сортировка алфавитного списка слов по длине сохранит все 8-буквенные слова в алфавитном порядке.
На самом деле я написал программу генерации кроссвордов около десяти лет назад (она была загадочной, но те же правила применялись и для обычных кроссвордов).
В нем был список слов (и связанных с ними подсказок), хранящийся в файле, отсортированный по убыванию использования на текущий момент (так, чтобы наименее используемые слова находились в верхней части файла). Шаблон, в основном битовая маска, представляющая черные и свободные квадраты, был выбран случайным образом из пула, предоставленного клиентом.
Затем для каждого неполного слова в головоломке (в основном найдите первый пустой квадрат и посмотрите, является ли пустым тот, который справа (поперечное слово) или тот, что внизу (нижнее слово)), был выполнен поиск файл ищет первое подходящее слово с учетом букв, уже присутствующих в этом слове. Если подходящего слова не было, вы просто отмечали все слово как неполное и переходили к следующему.
В конце будет несколько незавершенных слов, которые компилятор должен будет заполнить (и при желании добавить слово и подсказку к файлу). Если они не могли придумать какие-либо идеи, они могли вручную отредактировать кроссворд, чтобы изменить ограничения, или просто попросить его полностью пересоздать.
Как только файл слов / подсказок достигал определенного размера (а он добавлял 50-100 подсказок в день для этого клиента), редко возникало более двух или трех исправлений вручную, которые приходилось делать для каждого кроссворда. ,
Этот алгоритм создает 50 сложных кроссвордов со стрелками 6x9 за 60 секунд. Он использует базу данных слов (со словом + подсказками) и базу данных плат (с предварительно настроенными досками).
1) Search for all starting cells (the ones with an arrow), store their size and directions
2) Loop through all starting cells
2.1) Search a word
2.1.1) Check if it was not already used
2.1.2) Check if it fits
2.2) Add the word to the board
3) Check if all cells were filled
Большая база данных слов значительно сокращает время генерации, и некоторые доски сложнее заполнять! Для правильного заполнения больших досок требуется больше времени!
Пример:
Предварительно настроенная плата 6x9:
(# означает один наконечник в одной ячейке,% означает два наконечника в одной ячейке, стрелки не показаны)
# - # # - % # - #
- - - - - - - - -
# - - - - - # - -
% - - # - # - - -
% - - - - - % - -
- - - - - - - - -
Сгенерированная доска 6x9:
# C # # P % # O #
S A T E L L I T E
# N I N E S # T A
% A B # A # G A S
% D E N S E % W E
C A T H E D R A L
Советы [строка, столбец]:
[1,0] SATELLITE: Used for weather forecast
[5,0] CATHEDRAL: The principal church of a city
[0,1] CANADA: Country on USA's northern border
[0,4] PLEASE: A polite way to ask things
[0,7] OTTAWA: Canada's capital
[1,2] TIBET: Dalai Lama's region
[1,8] EASEL: A tripod used to put a painting
[2,1] NINES: Dressed up to (?)
[4,1] DENSE: Thick; impenetrable
[3,6] GAS: Type of fuel
[1,5] LS: Lori Singer, american actress
[2,7] TA: Teaching assistant (abbr.)
[3,1] AB: A blood type
[4,3] NH: New Hampshire (abbr.)
[4,5] ED: (?) Harris, american actor
[4,7] WE: The first person of plural (Grammar)
Хотя это более старый вопрос, попытаюсь ответить на основе аналогичной работы, которую я проделал.
Существует много подходов к решению проблем ограничений (которые обычно относятся к классу сложности NPC).
Это связано с комбинаторной оптимизацией и программированием в ограничениях. В этом случае ограничениями являются геометрия сетки и требование уникальности слов и т. Д.
Подходы рандомизации / отжига также могут работать (хотя и при правильных настройках).
Эффективная простота может быть высшей мудростью!
Требовалось наличие более или менее полного компилятора кроссвордов и (визуального WYSIWYG) построителя.
Не говоря уже о построении WYSIWYG, схема компилятора была такой:
Загрузить доступные словари (отсортированные по длине слова, например, 2,3, .., 20)
Найдите слоты слов (то есть слова сетки) в созданной пользователем сетке (например, слово в точках x, y с длиной L, по горизонтали или вертикали) (сложность O (N))
Вычислить точки пересечения слов сетки (которые необходимо заполнить) (сложность O (N ^ 2))
Вычислить пересечения слов в списках слов с различными буквами используемого алфавита (это позволяет искать совпадающие слова, используя шаблон, например, тезис Сика Камбона, используемый cwc ) (сложность O (WL * AL))
Шаги .3 и .4 позволяют выполнить эту задачу:
а. Пересечения слов сетки сами с собой позволяют создать «шаблон» для поиска совпадений в соответствующем списке слов доступных слов для этого слова сетки (с использованием букв других пересекающихся с этим словом слов, которые уже заполнены в определенное время). шаг алгоритма)
б. Пересечения слов в списке слов с алфавитом позволяют находить подходящие (кандидаты) слова, которые соответствуют заданному «шаблону» (например, «A» на 1-м месте и «B» на 3-м месте и т. Д.)
Итак, с реализованными этими структурами данных алгоритм был примерно таким:
ПРИМЕЧАНИЕ: если сетка и база слов постоянны, предыдущие шаги можно выполнить только один раз.
Первым шагом алгоритма является случайный выбор пустого слота слов (слово сетки) и заполнение его словом-кандидатом из связанного с ним списка слов (рандомизация позволяет производить различные решения при последовательном выполнении алгоритма) (сложность O (1) или O ( N))
Для каждого еще пустого слота слов (которые имеют пересечения с уже заполненными слотами слов) вычислите коэффициент ограничения (он может варьироваться, sth simple - количество доступных решений на этом шаге) и отсортируйте пустые слоты слов по этому соотношению (сложность O (NlogN ) или O (N))
Прокрутите пустые слоты слов, вычисленные на предыдущем шаге, и для каждого попробуйте несколько решений cancdidate (убедитесь, что «согласованность дуги сохраняется», т.е. сетка имеет решение после этого шага, если это слово используется) и отсортируйте их в соответствии с максимальная доступность для следующего шага (т.е. следующий шаг имеет максимально возможные решения, если это слово используется в то время в этом месте и т. д.) (сложность O (N * MaxCandidatesUsed))
Заполните это слово (отметьте его как заполненное и переходите к шагу 2)
Если не найдено ни одного слова, удовлетворяющего критериям шага .3, попробуйте вернуться к другому варианту решения некоторого предыдущего шага (здесь критерии могут отличаться) (сложность O (N))
Если обратный путь найден, используйте альтернативу и при необходимости сбросьте все уже заполненные слова, которые могут нуждаться в сбросе (снова пометьте их как незаполненные) (сложность O (N))
Если обратный путь не найден, решение не может быть найдено (по крайней мере, с этой конфигурацией, начальным начальным значением и т. Д.)
В противном случае, когда все словари заполнены, у вас есть одно решение
Этот алгоритм выполняет случайное последовательное обход дерева решений проблемы. Если в какой-то момент возникает тупик, он возвращается к предыдущему узлу и следует по другому маршруту. Пока не будет найдено решение или количество кандидатов для различных узлов не будет исчерпано.
Часть согласованности гарантирует, что найденное решение действительно является решением, а случайная часть позволяет создавать разные решения в разных исполнениях, а также в среднем иметь лучшую производительность.
PS. все это (и другие) было реализовано на чистом JavaScript (с параллельной обработкой и WYSIWYG).
PS2. Алгоритм можно легко распараллелить, чтобы получить более одного (разных) решений одновременно.
Надеюсь это поможет
Почему бы просто не использовать для начала случайный вероятностный подход. Начните со слова, а затем несколько раз выберите случайное слово и попытайтесь вписать его в текущее состояние головоломки, не нарушая ограничений на размер и т. Д. Если вы потерпите неудачу, просто начните все сначала.
Вы будете удивлены, как часто подобный подход Монте-Карло работает.
Вот код JavaScript, основанный на ответе никфа и коде Python Брайана. Просто разместите его на случай, если кому-то это понадобится в js.
function board(cols, rows) { //instantiator object for making gameboards
this.cols = cols;
this.rows = rows;
var activeWordList = []; //keeps array of words actually placed in board
var acrossCount = 0;
var downCount = 0;
var grid = new Array(cols); //create 2 dimensional array for letter grid
for (var i = 0; i < rows; i++) {
grid[i] = new Array(rows);
}
for (var x = 0; x < cols; x++) {
for (var y = 0; y < rows; y++) {
grid[x][y] = {};
grid[x][y].targetChar = EMPTYCHAR; //target character, hidden
grid[x][y].indexDisplay = ''; //used to display index number of word start
grid[x][y].value = '-'; //actual current letter shown on board
}
}
function suggestCoords(word) { //search for potential cross placement locations
var c = '';
coordCount = [];
coordCount = 0;
for (i = 0; i < word.length; i++) { //cycle through each character of the word
for (x = 0; x < GRID_HEIGHT; x++) {
for (y = 0; y < GRID_WIDTH; y++) {
c = word[i];
if (grid[x][y].targetChar == c) { //check for letter match in cell
if (x - i + 1> 0 && x - i + word.length-1 < GRID_HEIGHT) { //would fit vertically?
coordList[coordCount] = {};
coordList[coordCount].x = x - i;
coordList[coordCount].y = y;
coordList[coordCount].score = 0;
coordList[coordCount].vertical = true;
coordCount++;
}
if (y - i + 1 > 0 && y - i + word.length-1 < GRID_WIDTH) { //would fit horizontally?
coordList[coordCount] = {};
coordList[coordCount].x = x;
coordList[coordCount].y = y - i;
coordList[coordCount].score = 0;
coordList[coordCount].vertical = false;
coordCount++;
}
}
}
}
}
}
function checkFitScore(word, x, y, vertical) {
var fitScore = 1; //default is 1, 2+ has crosses, 0 is invalid due to collision
if (vertical) { //vertical checking
for (i = 0; i < word.length; i++) {
if (i == 0 && x > 0) { //check for empty space preceeding first character of word if not on edge
if (grid[x - 1][y].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
} else if (i == word.length && x < GRID_HEIGHT) { //check for empty space after last character of word if not on edge
if (grid[x+i+1][y].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
if (x + i < GRID_HEIGHT) {
if (grid[x + i][y].targetChar == word[i]) { //letter match - aka cross point
fitScore += 1;
} else if (grid[x + i][y].targetChar != EMPTYCHAR) { //letter doesn't match and it isn't empty so there is a collision
fitScore = 0;
break;
} else { //verify that there aren't letters on either side of placement if it isn't a crosspoint
if (y < GRID_WIDTH - 1) { //check right side if it isn't on the edge
if (grid[x + i][y + 1].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
if (y > 0) { //check left side if it isn't on the edge
if (grid[x + i][y - 1].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
}
}
}
} else { //horizontal checking
for (i = 0; i < word.length; i++) {
if (i == 0 && y > 0) { //check for empty space preceeding first character of word if not on edge
if (grid[x][y-1].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
} else if (i == word.length - 1 && y + i < GRID_WIDTH -1) { //check for empty space after last character of word if not on edge
if (grid[x][y + i + 1].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
if (y + i < GRID_WIDTH) {
if (grid[x][y + i].targetChar == word[i]) { //letter match - aka cross point
fitScore += 1;
} else if (grid[x][y + i].targetChar != EMPTYCHAR) { //letter doesn't match and it isn't empty so there is a collision
fitScore = 0;
break;
} else { //verify that there aren't letters on either side of placement if it isn't a crosspoint
if (x < GRID_HEIGHT) { //check top side if it isn't on the edge
if (grid[x + 1][y + i].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
if (x > 0) { //check bottom side if it isn't on the edge
if (grid[x - 1][y + i].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
}
}
}
}
return fitScore;
}
function placeWord(word, clue, x, y, vertical) { //places a new active word on the board
var wordPlaced = false;
if (vertical) {
if (word.length + x < GRID_HEIGHT) {
for (i = 0; i < word.length; i++) {
grid[x + i][y].targetChar = word[i];
}
wordPlaced = true;
}
} else {
if (word.length + y < GRID_WIDTH) {
for (i = 0; i < word.length; i++) {
grid[x][y + i].targetChar = word[i];
}
wordPlaced = true;
}
}
if (wordPlaced) {
var currentIndex = activeWordList.length;
activeWordList[currentIndex] = {};
activeWordList[currentIndex].word = word;
activeWordList[currentIndex].clue = clue;
activeWordList[currentIndex].x = x;
activeWordList[currentIndex].y = y;
activeWordList[currentIndex].vertical = vertical;
if (activeWordList[currentIndex].vertical) {
downCount++;
activeWordList[currentIndex].number = downCount;
} else {
acrossCount++;
activeWordList[currentIndex].number = acrossCount;
}
}
}
function isActiveWord(word) {
if (activeWordList.length > 0) {
for (var w = 0; w < activeWordList.length; w++) {
if (word == activeWordList[w].word) {
//console.log(word + ' in activeWordList');
return true;
}
}
}
return false;
}
this.displayGrid = function displayGrid() {
var rowStr = "";
for (var x = 0; x < cols; x++) {
for (var y = 0; y < rows; y++) {
rowStr += "<td>" + grid[x][y].targetChar + "</td>";
}
$('#tempTable').append("<tr>" + rowStr + "</tr>");
rowStr = "";
}
console.log('across ' + acrossCount);
console.log('down ' + downCount);
}
//for each word in the source array we test where it can fit on the board and then test those locations for validity against other already placed words
this.generateBoard = function generateBoard(seed = 0) {
var bestScoreIndex = 0;
var top = 0;
var fitScore = 0;
var startTime;
//manually place the longest word horizontally at 0,0, try others if the generated board is too weak
placeWord(wordArray[seed].word, wordArray[seed].displayWord, wordArray[seed].clue, 0, 0, false);
//attempt to fill the rest of the board
for (var iy = 0; iy < FIT_ATTEMPTS; iy++) { //usually 2 times is enough for max fill potential
for (var ix = 1; ix < wordArray.length; ix++) {
if (!isActiveWord(wordArray[ix].word)) { //only add if not already in the active word list
topScore = 0;
bestScoreIndex = 0;
suggestCoords(wordArray[ix].word); //fills coordList and coordCount
coordList = shuffleArray(coordList); //adds some randomization
if (coordList[0]) {
for (c = 0; c < coordList.length; c++) { //get the best fit score from the list of possible valid coordinates
fitScore = checkFitScore(wordArray[ix].word, coordList[c].x, coordList[c].y, coordList[c].vertical);
if (fitScore > topScore) {
topScore = fitScore;
bestScoreIndex = c;
}
}
}
if (topScore > 1) { //only place a word if it has a fitscore of 2 or higher
placeWord(wordArray[ix].word, wordArray[ix].clue, coordList[bestScoreIndex].x, coordList[bestScoreIndex].y, coordList[bestScoreIndex].vertical);
}
}
}
}
if(activeWordList.length < wordArray.length/2) { //regenerate board if if less than half the words were placed
seed++;
generateBoard(seed);
}
}
}
function seedBoard() {
gameboard = new board(GRID_WIDTH, GRID_HEIGHT);
gameboard.generateBoard();
gameboard.displayGrid();
}
Я бы получил два числа: длину и оценку скрэббла. Предположим, что низкий балл по Scrabble означает, что присоединиться к нему легче (низкие баллы = много общих букв). Отсортируйте список по убыванию длины и возрастанию баллов Scrabble.
Далее просто пройдите вниз по списку. Если слово не пересекается с существующим словом (проверьте каждое слово по его длине и баллу Scrabble соответственно), поместите его в очередь и проверьте следующее слово.
Промойте и повторите, и это должен создать кроссворд.
Конечно, я почти уверен, что это О (н!), И это не гарантирует, что вы решите кроссворд за вас, но, возможно, кто-то сможет его улучшить.
Я думал об этой проблеме. Я считаю, что для создания действительно сложного кроссворда нельзя надеяться, что вашего ограниченного списка слов будет достаточно. Следовательно, вы можете взять словарь и поместить его в структуру данных «trie». Это позволит вам легко находить слова, заполняющие оставшиеся пробелы. В некотором роде довольно эффективно реализовать обход, который, скажем, дает вам все слова формы «c? T».
Итак, мое общее мнение таково: создайте своего рода подход относительно грубой силы, как некоторые описывают здесь, чтобы создать крест с низкой плотностью, и заполните пробелы словарными словами.
Если кто-то еще воспользовался этим подходом, дайте мне знать.
Я играл с двигателем генератора кроссвордов, и я нашел это самым важным:
0.!/usr/bin/python
а. allwords.sort(key=len, reverse=True)
б. создайте какой-нибудь элемент / объект, например курсор, который будет перемещаться по матрице для облегчения ориентации, если вы не хотите повторять случайный выбор позже.
первый, возьмите первую пару и разместите их поперек и вниз от 0,0; сохранить первый из них как «лидер» нашего текущего кроссворда.
перемещать курсор по диагонали по порядку или случайным образом с большей вероятностью диагонали в следующую пустую ячейку
перебирать слова вроде и использовать длину свободного пространства для определения максимальной длины слова:
temp=[]
for w_size in range( len( w_space ), 2, -1 ) :
# t
for w in [ word for word in allwords if len(word) == w_size ] :
#
if w not in temp and putTheWord( w, w_space ) :
#
temp.append( w )
чтобы сравнить слово со свободным пространством, я использовал, например:
w_space=['c','.','a','.','.','.'] # whereas dots are blank cells
# CONVERT MULTIPLE '.' INTO '.*' FOR REGEX
pattern = r''.join( [ x.letter for x in w_space ] )
pattern = pattern.strip('.') +'.*' if pattern[-1] == '.' else pattern
prog = re.compile( pattern, re.U | re.I )
if prog.match( w ) :
#
if prog.match( w ).group() == w :
#
return True
после каждого удачно употребленного слова меняйте направление. Цикл, пока все ячейки заполнены ИЛИ у вас закончились слова ИЛИ на лимит итераций, тогда:
# CHANGE ALL WORDS LIST
inexOf1stWord = allwords.index( leading_w )
allwords = allwords[:inexOf1stWord+1][:] + allwords[inexOf1stWord+1:][:]
... и повторить заново новый кроссворд.
Составьте систему оценок по легкости заполнения и некоторым оценочным расчетам. Поставьте оценку за текущий кроссворд и сузьте последующий выбор, добавив его в список созданных кроссвордов, если оценка соответствует вашей системе подсчета очков.
После первого сеанса итерации повторите итерацию из списка составленных кроссвордов, чтобы завершить задание.
Используя больше параметров, скорость можно значительно улучшить.
Я бы получил указатель каждой буквы, используемой в каждом слове, чтобы знать возможные кресты. Затем я выбирал самое большое слово и использовал его как основу. Выберите следующий большой и пересеките его. Промыть и повторить. Вероятно, это проблема NP.
Еще одна идея - создать генетический алгоритм, в котором показатель силы - это количество слов, которые вы можете поместить в сетку.
Самая сложная часть, которую я нахожу, - это когда знать, что определенный список невозможно перечеркнуть.
Он появляется как проект в курсе AI CS50 из Гарварда. Идея состоит в том, чтобы сформулировать проблему генерации кроссворда как проблему удовлетворения ограничений и решить ее с помощью обратного отслеживания с различными эвристиками, чтобы уменьшить пространство поиска.
Для начала нам понадобится пара входных файлов:
`
###_####_#
____####_#
_##_#_____
_##_#_##_#
______####
#_###_####
#_##______
#_###_##_#
_____###_#
#_######_#
##_______#
`
Входной словарь (список слов / словарь), из которого будут выбраны слова-кандидаты (как показано ниже).
a
abandon
ability
able
abortion
about
above
abroad
absence
absolute
absolutely
...
Теперь CSP определен и должен быть решен следующим образом:
Ниже показан результат, который был получен с использованием реализации алгоритма решения CSP:
`
███S████D█
MUCH████E█
E██A█AGENT
S██R█N██Y█
SUPPLY████
█N███O████
█I██INSIDE
█Q███E██A█
SUGAR███N█
█E██████C█
██OFFENSE█
`
Следующая анимация показывает шаги возврата:
Вот еще одно слово со списком слов на языке бангла (бенгали):
Я написал решение этой проблемы на JavaScript / jQuery:
Пример демонстрации: http://www.earthfluent.com/crossword-puzzle-demo.html
Исходный код: https://github.com/HoldOffHunger/jquery-crossword-puzzle-generator
Цель алгоритма, который я использовал:
Опишу используемый мной алгоритм:
Сгруппируйте слова вместе по тем, которые имеют общую букву.
Из этих групп постройте наборы новой структуры данных («блоки слов»), которые представляют собой первичное слово (которое проходит через все другие слова), а затем другие слова (которые проходят через первичное слово).
Начните разгадывать кроссворд с самого первого из этих блоков слов в самом верхнем левом положении кроссворда.
Для остальных блоков слов, начиная с самого правого нижнего положения кроссворда, двигайтесь вверх и влево, пока не останется свободных мест для заполнения. Если вверху больше пустых столбцов, чем влево, двигайтесь вверх и наоборот.
var crosswords = generateCrosswordBlockSources(puzzlewords);
. Просто запишите это значение в консоль. Не забывайте, что в игре есть «чит-режим», где вы можете просто нажать «Показать ответ», чтобы сразу получить значение.