Алгоритм генерации кроссворда

123

Имея список слов, как бы вы собрали их в сетку кроссвордов?

Это не должно быть похоже на «правильный» кроссворд, который является симметричным или что-то в этом роде: в основном просто выводите начальную позицию и направление для каждого слова.

nickf
источник

Ответы:

62

Я придумал решение, которое, вероятно, не самое эффективное, но работает достаточно хорошо. В принципе:

  1. Отсортируйте все слова по длине в порядке убывания.
  2. Возьмите первое слово и поместите его на доску.
  3. Возьми следующее слово.
  4. Просмотрите все слова, которые уже есть на доске, и посмотрите, есть ли какие-либо возможные пересечения (любые общие буквы) с этим словом.
  5. Если есть возможное место для этого слова, прокрутите все слова, которые есть на доске, и проверьте, не мешает ли новое слово.
  6. Если это слово не сломает доску, поместите его туда и переходите к шагу 3, в противном случае продолжайте поиск места (шаг 4).
  7. Продолжайте этот цикл, пока все слова не будут размещены или не могут быть размещены.

Это рабочий, но зачастую довольно плохой кроссворд. Я внес ряд изменений в основной рецепт, приведенный выше, чтобы добиться лучшего результата.

  • В конце создания кроссворда дайте ему оценку в зависимости от того, сколько слов было размещено (чем больше, тем лучше), насколько велика доска (чем меньше, тем лучше), а также соотношение между высотой и шириной (чем ближе к 1 лучше). Сгенерируйте несколько кроссвордов, затем сравните их результаты и выберите лучший.
    • Вместо того, чтобы запускать произвольное количество итераций, я решил создать как можно больше кроссвордов за произвольный промежуток времени. Если у вас есть только небольшой список слов, вы получите десятки возможных кроссвордов за 5 секунд. Кроссворд побольше можно выбрать только из 5-6 возможных.
  • При размещении нового слова вместо того, чтобы размещать его сразу после нахождения приемлемого местоположения, присвойте этому местоположению слова оценку в зависимости от того, насколько оно увеличивает размер сетки и сколько пересечений имеется (в идеале вы хотите, чтобы каждое слово было скрещено еще 2-3 слова). Следите за всеми позициями и их счетами, а затем выбирайте лучшую.
nickf
источник
7
Я пишу эту программу, пока мы говорим, и это тот же алгоритм, который я выбрал. Для небольшого количества слов (10 или меньше) программа без труда рассчитала все возможные решения за миллисекунды. Однако алгоритм экспоненциальный. Самая простая часть - написать базовый алгоритм, который перебирает все возможные комбинации. Самая сложная часть - это дюжина или около того «сокращений», которые вам понадобятся, чтобы программа не испробовала все тупиковые решения.
user31586
2
«5.… и проверьте, не мешает ли новое слово» Как вы учитываете ситуации, когда новое слово стоит рядом с существующим словом, что порождает тарабарщину в местах, где к нему примыкают квадраты? Например: LEMON ERASE Если «LE», «ER», «MA» и т. Д. Не являются словами в вашем списке, это неправильно. С другой стороны, прямой отказ от таких смежностей может отбросить действительно хорошие сетки, например: W LEMON ERASE NEXUS TT
Джордж Армхолд,
4
@Kaffeine, да, я знаю, что вы имеете в виду - мне пришлось выбросить эти варианты, потому что, хотя они могут создавать действительно хорошие сетки, их слишком сложно проверить (читайте: я не мог беспокоиться) , и есть вероятность, что это просто помехи в любом случае ,
nickf
4
Создан с использованием jQuery / Javascript с использованием приведенных выше рекомендаций и некоторых моих собственных ... mlewiscs.com/crossword
MLewisCodeSolutions
@MLewisCodeSolutions Выглядит потрясающе. Вы открываете исходный код?
ГКС
23

Я недавно написал свой собственный на Python. Вы можете найти его здесь: http://bryanhelmig.com/python-crossword-puzzle-generator/ . Он создает не сложные кроссворды в стиле NYT, а стиль кроссвордов, который вы можете найти в детской книжке-головоломке.

В отличие от нескольких алгоритмов, которые я обнаружил там, которые реализовали случайный метод грубой силы для размещения слов, как предлагали некоторые, я попытался реализовать немного более умный подход грубой силы при размещении слов. Вот мой процесс:

  1. Создайте сетку любого размера и список слов.
  2. Перемешайте список слов, а затем отсортируйте слова от самого длинного к самому короткому.
  3. Поместите первое и самое длинное слово в крайнее левое верхнее положение, 1,1 (вертикальное или горизонтальное).
  4. Перейдите к следующему слову, переберите каждую букву в слове и каждую ячейку в сетке, ища совпадения букв.
  5. Когда совпадение найдено, просто добавьте эту позицию в предлагаемый список координат для этого слова.
  6. Прокрутите предложенный список координат и «оцените» размещение слова в зависимости от того, сколько других слов оно пересекает. Оценка 0 указывает либо на плохое размещение (рядом с существующими словами), либо на отсутствие крестиков.
  7. Вернитесь к шагу №4, пока список слов не будет исчерпан. Дополнительный второй проход.
  8. Теперь у нас должен быть кроссворд, но его качество может быть плохим из-за некоторых случайных размещений. Итак, буферизуем кроссворд и возвращаемся к шагу №2. Если следующий кроссворд содержит больше слов на доске, он заменяет кроссворд в буфере. Время ограничено (найдите лучший кроссворд за x секунд).

В конце концов, у вас есть приличный кроссворд или головоломка для поиска слов, поскольку они примерно одинаковы. Как правило, он работает довольно хорошо, но дайте мне знать, если у вас есть предложения по улучшению. Более крупные сети работают экспоненциально медленнее; линейно большие списки слов. Списки слов большего размера также имеют гораздо больше шансов на лучшее размещение слов.

Bryan
источник
@Neil N: Вероятно, лучшая возможность сопоставления букв для других слов. Возможно, это был бы подход к сортировке по количеству отдельных букв, содержащихся в слове, что в основном приведет к тому же результату.
Карл Адлер,
@NeilN Python array.sort(key=f)является стабильным, что означает (например), что простая сортировка алфавитного списка слов по длине сохранит все 8-буквенные слова в алфавитном порядке.
Линн
4
@Bryan, ссылка на ваш сайт у меня не работает, а основной домен просто перенаправляет на Twitter. У вас есть обновленная ссылка на ваш код?
Michael A
2
Вот (по-видимому) клон генератора Брайана: github.com/jeremy886/crossword_helmig
lvictorino
20

На самом деле я написал программу генерации кроссвордов около десяти лет назад (она была загадочной, но те же правила применялись и для обычных кроссвордов).

В нем был список слов (и связанных с ними подсказок), хранящийся в файле, отсортированный по убыванию использования на текущий момент (так, чтобы наименее используемые слова находились в верхней части файла). Шаблон, в основном битовая маска, представляющая черные и свободные квадраты, был выбран случайным образом из пула, предоставленного клиентом.

Затем для каждого неполного слова в головоломке (в основном найдите первый пустой квадрат и посмотрите, является ли пустым тот, который справа (поперечное слово) или тот, что внизу (нижнее слово)), был выполнен поиск файл ищет первое подходящее слово с учетом букв, уже присутствующих в этом слове. Если подходящего слова не было, вы просто отмечали все слово как неполное и переходили к следующему.

В конце будет несколько незавершенных слов, которые компилятор должен будет заполнить (и при желании добавить слово и подсказку к файлу). Если они не могли придумать какие-либо идеи, они могли вручную отредактировать кроссворд, чтобы изменить ограничения, или просто попросить его полностью пересоздать.

Как только файл слов / подсказок достигал определенного размера (а он добавлял 50-100 подсказок в день для этого клиента), редко возникало более двух или трех исправлений вручную, которые приходилось делать для каждого кроссворда. ,

paxdiablo
источник
На самом деле это не помогает мне в моей ситуации, так как у меня есть список всего из 6-12 слов. Моя для пользователя больше похожа на обучающее упражнение, чем на словесную головоломку. +1 хотя бы за интересный алгоритм!
nickf 03
1
Красивое описание. Я думал об этом несколько раз в прошлом, но никогда не пробовал. Теперь волшебный вопрос: насколько хорошо это сработало? Просто для разреженных головоломок или еще и для плотных (как в бумаге)? А сколько подсказок нужно для сложных головоломок?
dmckee --- котенок экс-модератора
1
@dmckee, это было давно, но по памяти даже сложные головоломки были довольно хороши. Многие из них были выполнены без вмешательства, но вы все равно получите, возможно, пятый, требующий добавления одного или двух дополнительных слов. И мы говорим о тысячах слов в файле. Несомненно, обратное отслеживание могло бы помочь, но для клиента было легче просто отклонить одно, например, с пятью незавершенными словами, чем беспокоиться о попытках придумать дополнительные подсказки. Пять - это предел, который я видел для незаконченных слов.
paxdiablo
16

Этот алгоритм создает 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)
thiagolr
источник
11

Хотя это более старый вопрос, попытаюсь ответить на основе аналогичной работы, которую я проделал.

Существует много подходов к решению проблем ограничений (которые обычно относятся к классу сложности NPC).

Это связано с комбинаторной оптимизацией и программированием в ограничениях. В этом случае ограничениями являются геометрия сетки и требование уникальности слов и т. Д.

Подходы рандомизации / отжига также могут работать (хотя и при правильных настройках).

Эффективная простота может быть высшей мудростью!

Требовалось наличие более или менее полного компилятора кроссвордов и (визуального WYSIWYG) построителя.

Не говоря уже о построении WYSIWYG, схема компилятора была такой:

  1. Загрузить доступные словари (отсортированные по длине слова, например, 2,3, .., 20)

  2. Найдите слоты слов (то есть слова сетки) в созданной пользователем сетке (например, слово в точках x, y с длиной L, по горизонтали или вертикали) (сложность O (N))

  3. Вычислить точки пересечения слов сетки (которые необходимо заполнить) (сложность O (N ^ 2))

  4. Вычислить пересечения слов в списках слов с различными буквами используемого алфавита (это позволяет искать совпадающие слова, используя шаблон, например, тезис Сика Камбона, используемый cwc ) (сложность O (WL * AL))

Шаги .3 и .4 позволяют выполнить эту задачу:

а. Пересечения слов сетки сами с собой позволяют создать «шаблон» для поиска совпадений в соответствующем списке слов доступных слов для этого слова сетки (с использованием букв других пересекающихся с этим словом слов, которые уже заполнены в определенное время). шаг алгоритма)

б. Пересечения слов в списке слов с алфавитом позволяют находить подходящие (кандидаты) слова, которые соответствуют заданному «шаблону» (например, «A» на 1-м месте и «B» на 3-м месте и т. Д.)

Итак, с реализованными этими структурами данных алгоритм был примерно таким:

ПРИМЕЧАНИЕ: если сетка и база слов постоянны, предыдущие шаги можно выполнить только один раз.

  1. Первым шагом алгоритма является случайный выбор пустого слота слов (слово сетки) и заполнение его словом-кандидатом из связанного с ним списка слов (рандомизация позволяет производить различные решения при последовательном выполнении алгоритма) (сложность O (1) или O ( N))

  2. Для каждого еще пустого слота слов (которые имеют пересечения с уже заполненными слотами слов) вычислите коэффициент ограничения (он может варьироваться, sth simple - количество доступных решений на этом шаге) и отсортируйте пустые слоты слов по этому соотношению (сложность O (NlogN ) или O (N))

  3. Прокрутите пустые слоты слов, вычисленные на предыдущем шаге, и для каждого попробуйте несколько решений cancdidate (убедитесь, что «согласованность дуги сохраняется», т.е. сетка имеет решение после этого шага, если это слово используется) и отсортируйте их в соответствии с максимальная доступность для следующего шага (т.е. следующий шаг имеет максимально возможные решения, если это слово используется в то время в этом месте и т. д.) (сложность O (N * MaxCandidatesUsed))

  4. Заполните это слово (отметьте его как заполненное и переходите к шагу 2)

  5. Если не найдено ни одного слова, удовлетворяющего критериям шага .3, попробуйте вернуться к другому варианту решения некоторого предыдущего шага (здесь критерии могут отличаться) (сложность O (N))

  6. Если обратный путь найден, используйте альтернативу и при необходимости сбросьте все уже заполненные слова, которые могут нуждаться в сбросе (снова пометьте их как незаполненные) (сложность O (N))

  7. Если обратный путь не найден, решение не может быть найдено (по крайней мере, с этой конфигурацией, начальным начальным значением и т. Д.)

  8. В противном случае, когда все словари заполнены, у вас есть одно решение

Этот алгоритм выполняет случайное последовательное обход дерева решений проблемы. Если в какой-то момент возникает тупик, он возвращается к предыдущему узлу и следует по другому маршруту. Пока не будет найдено решение или количество кандидатов для различных узлов не будет исчерпано.

Часть согласованности гарантирует, что найденное решение действительно является решением, а случайная часть позволяет создавать разные решения в разных исполнениях, а также в среднем иметь лучшую производительность.

PS. все это (и другие) было реализовано на чистом JavaScript (с параллельной обработкой и WYSIWYG).

PS2. Алгоритм можно легко распараллелить, чтобы получить более одного (разных) решений одновременно.

Надеюсь это поможет

Никос М.
источник
1
Это для создания плотных макетов (например, NY Times) или разреженных макетов?
Джим
1
@Jim, это в основном для плотных макетов, но также можно настроить разреженные. Разница в том, что в плотных макетах (например, классический, скандинавский и т. Д.) Есть сетка и поиск слов, тогда как для произвольных макетов (разреженных) есть слова и поиск сетки.
Nikos M.
1
Не могли бы вы где-нибудь найти образец источника, реализующий описанные выше шаги. Например, я с вами в отношении большей части этого (и уже реализовал большую часть из них независимо), но когда дело доходит до «вычисления отношения ограничений ...», я должен признать, что вы меня потеряли. Поиск в Интернете таких вещей, как "Соотношение STH", тоже мне не очень помогает. Проблема с моей реализацией заключается в том, что попытка найти слова для заполнения очень неэффективна и занимает слишком много времени.
Джим
1
@ Джим, у меня есть, поскольку это уже используется, но это было сделано специально для моей работы, возможно, я опубликую облегченную версию в своих проектах с открытым исходным кодом, если вам нужна дополнительная помощь, свяжитесь со мной (действительно, в в некоторых случаях алгоритм, который я опубликовал, может занять слишком много времени, но в среднем это не так)
Никос М.
1
@Jim, взгляните на этот сайт кроссвордов (еще в разработке) istavrolexo.gr (на греческом) с различными (плотными) кроссвордами (например, скандинавские, классические, судоку), которые были созданы с помощью аналогичного алгоритма ( большой скандинавский кроссворд )
Никос М.
9

Почему бы просто не использовать для начала случайный вероятностный подход. Начните со слова, а затем несколько раз выберите случайное слово и попытайтесь вписать его в текущее состояние головоломки, не нарушая ограничений на размер и т. Д. Если вы потерпите неудачу, просто начните все сначала.

Вы будете удивлены, как часто подобный подход Монте-Карло работает.

Йог
источник
2
да, я выбрал именно такой подход. Вам не нужно стараться быть очень умным. Упорядочивайте слова от самого длинного к самому короткому. В цикле выберите случайную ячейку (координаты столбца и строки) и поместите слово на доску, чтобы проверить, не выходит ли оно за конец или мешает другому слову (перед тем, как записать слово в сетку, убедитесь, что каждая ячейка либо пусто или, если в нем есть буква, эта буква соответствует букве, которую вы пытаетесь написать). Есть другая логика для проверки границ и прочего. Я грубой силой генерирую кучу все меньших и меньших сеток, а затем ранжирую самые маленькие на основе пересекающихся слов.
Макс Ходжес,
6

Вот код 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();
}
FascistDonut
источник
Схема объекта word отсутствует, укажите wordArray
т.е.
Буквально просто набор таких слов, как ['яблоко', 'апельсин', 'груша']
FascistDonut
Привет, к сведению, мое редактирование не изменило много кода, оно просто отформатировало его. Я знаю, что при «встроенном» просмотре он выглядит таким беспорядочным, но если вы хотите увидеть реальные изменения в коде, нажмите «бок о бок с уценкой». Что ж ... Я должен был написать «Отформатированный код» в описании редактирования, но м-м-м.
двойной сигнал
Как это работает? Можете ли вы предоставить html-файл, содержащий этот javascript?
GKS
5

Я бы получил два числа: длину и оценку скрэббла. Предположим, что низкий балл по Scrabble означает, что присоединиться к нему легче (низкие баллы = много общих букв). Отсортируйте список по убыванию длины и возрастанию баллов Scrabble.

Далее просто пройдите вниз по списку. Если слово не пересекается с существующим словом (проверьте каждое слово по его длине и баллу Scrabble соответственно), поместите его в очередь и проверьте следующее слово.

Промойте и повторите, и это должен создать кроссворд.

Конечно, я почти уверен, что это О (н!), И это не гарантирует, что вы решите кроссворд за вас, но, возможно, кто-то сможет его улучшить.

Эрик
источник
3

Я думал об этой проблеме. Я считаю, что для создания действительно сложного кроссворда нельзя надеяться, что вашего ограниченного списка слов будет достаточно. Следовательно, вы можете взять словарь и поместить его в структуру данных «trie». Это позволит вам легко находить слова, заполняющие оставшиеся пробелы. В некотором роде довольно эффективно реализовать обход, который, скажем, дает вам все слова формы «c? T».

Итак, мое общее мнение таково: создайте своего рода подход относительно грубой силы, как некоторые описывают здесь, чтобы создать крест с низкой плотностью, и заполните пробелы словарными словами.

Если кто-то еще воспользовался этим подходом, дайте мне знать.

Джейк
источник
3

Я играл с двигателем генератора кроссвордов, и я нашел это самым важным:

0.!/usr/bin/python

  1. а. allwords.sort(key=len, reverse=True)

    б. создайте какой-нибудь элемент / объект, например курсор, который будет перемещаться по матрице для облегчения ориентации, если вы не хотите повторять случайный выбор позже.

  2. первый, возьмите первую пару и разместите их поперек и вниз от 0,0; сохранить первый из них как «лидер» нашего текущего кроссворда.

  3. перемещать курсор по диагонали по порядку или случайным образом с большей вероятностью диагонали в следующую пустую ячейку

  4. перебирать слова вроде и использовать длину свободного пространства для определения максимальной длины слова: 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 )

  5. чтобы сравнить слово со свободным пространством, я использовал, например:

    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
    
  6. после каждого удачно употребленного слова меняйте направление. Цикл, пока все ячейки заполнены ИЛИ у вас закончились слова ИЛИ на лимит итераций, тогда:

# CHANGE ALL WORDS LIST inexOf1stWord = allwords.index( leading_w ) allwords = allwords[:inexOf1stWord+1][:] + allwords[inexOf1stWord+1:][:]

... и повторить заново новый кроссворд.

  1. Составьте систему оценок по легкости заполнения и некоторым оценочным расчетам. Поставьте оценку за текущий кроссворд и сузьте последующий выбор, добавив его в список созданных кроссвордов, если оценка соответствует вашей системе подсчета очков.

  2. После первого сеанса итерации повторите итерацию из списка составленных кроссвордов, чтобы завершить задание.

Используя больше параметров, скорость можно значительно улучшить.

Alex
источник
2

Я бы получил указатель каждой буквы, используемой в каждом слове, чтобы знать возможные кресты. Затем я выбирал самое большое слово и использовал его как основу. Выберите следующий большой и пересеките его. Промыть и повторить. Вероятно, это проблема NP.

Еще одна идея - создать генетический алгоритм, в котором показатель силы - это количество слов, которые вы можете поместить в сетку.

Самая сложная часть, которую я нахожу, - это когда знать, что определенный список невозможно перечеркнуть.

eipipuz
источник
1
Я также думал о генетическом алгоритме. Фитнес-функция может заключаться в том, насколько плотно слова упакованы в сетку.
Адриан МакКарти,
2

Он появляется как проект в курсе AI CS50 из Гарварда. Идея состоит в том, чтобы сформулировать проблему генерации кроссворда как проблему удовлетворения ограничений и решить ее с помощью обратного отслеживания с различными эвристиками, чтобы уменьшить пространство поиска.

Для начала нам понадобится пара входных файлов:

  1. Структура кроссворда (которая выглядит как следующая, например, где '#' представляет символы, которые не следует заполнять, а '_' представляет символы, которые необходимо заполнить)

`

###_####_#
____####_#
_##_#_____
_##_#_##_#
______####
#_###_####
#_##______
#_###_##_#
_____###_#
#_######_#
##_______#    

`

  1. Входной словарь (список слов / словарь), из которого будут выбраны слова-кандидаты (как показано ниже).

    a abandon ability able abortion about above abroad absence absolute absolutely ...

Теперь CSP определен и должен быть решен следующим образом:

  1. Переменные определяются как имеющие значения (т. Е. Их домены) из списка слов (словаря), предоставленного в качестве входных данных.
  2. Каждая переменная представлена ​​тремя кортежами: (координата сетки, направление, длина), где координата представляет начало соответствующего слова, направление может быть горизонтальным или вертикальным, а длина определяется как длина слова, которое будет назначен.
  3. Ограничения определяются входными данными структуры: например, если горизонтальная и вертикальная переменные имеют общий символ, это будет представлено как ограничение перекрытия (дуги).
  4. Теперь можно использовать алгоритмы согласованности узлов и согласованности дуги AC3 для уменьшения количества доменов.
  5. Затем для получения решения (если оно существует) для CSP с помощью MRV (минимальное оставшееся значение), степени и т. Д. Эвристика может использоваться для выбора следующей неназначенной переменной, а эвристика, такая как LCV (наименьшее ограничивающее значение), может использоваться для домена: упорядочивание, чтобы алгоритм поиска работал быстрее.

Ниже показан результат, который был получен с использованием реализации алгоритма решения 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█

`

Следующая анимация показывает шаги возврата:

введите описание изображения здесь

Вот еще одно слово со списком слов на языке бангла (бенгали):

введите описание изображения здесь

Сандипан Дей
источник
+1 за действительно интересное объяснение. Однако контекст для проблемы, которую я пытался решить здесь, заключался в том, что был небольшой набор слов, которые все должны были использоваться, и я пытался найти оптимальную раскладку кроссворда, вместо того, чтобы начинать с раскладки и находить слова которые подходят.
nickf
1

Генератор кроссвордов jQuery и игра

Я написал решение этой проблемы на JavaScript / jQuery:

Пример демонстрации: http://www.earthfluent.com/crossword-puzzle-demo.html

Исходный код: https://github.com/HoldOffHunger/jquery-crossword-puzzle-generator

Цель алгоритма, который я использовал:

  1. Сведите к минимуму количество неиспользуемых квадратов в сетке, насколько это возможно.
  2. Перемешайте как можно больше слов.
  3. Вычисляйте за очень короткое время.

Демонстрация сгенерированного кроссворда.

Опишу используемый мной алгоритм:

  1. Сгруппируйте слова вместе по тем, которые имеют общую букву.

  2. Из этих групп постройте наборы новой структуры данных («блоки слов»), которые представляют собой первичное слово (которое проходит через все другие слова), а затем другие слова (которые проходят через первичное слово).

  3. Начните разгадывать кроссворд с самого первого из этих блоков слов в самом верхнем левом положении кроссворда.

  4. Для остальных блоков слов, начиная с самого правого нижнего положения кроссворда, двигайтесь вверх и влево, пока не останется свободных мест для заполнения. Если вверху больше пустых столбцов, чем влево, двигайтесь вверх и наоборот.

HoldOffHunger
источник
@holdoffhunger У вас есть способ показать ключ кроссворда? Ящики с заполненными буквами?
Jon Glazer
@Jon Glazer: Как правило, вы отправляете ключи кроссворда самой функции, но вы можете записать кроссворд как 2d-массив символов сразу после этого var crosswords = generateCrosswordBlockSources(puzzlewords);. Просто запишите это значение в консоль. Не забывайте, что в игре есть «чит-режим», где вы можете просто нажать «Показать ответ», чтобы сразу получить значение.
HoldOffHunger
Это создает головоломки с тарабарщиной «поперек» слов в местах с соседними прямоугольниками «вниз», и наоборот. Стандартные кроссворды не работают так, хотя это делает максимальную плотность.
Beejor,