Відповіді:
Я придумав рішення, яке, мабуть, не найефективніше, але воно працює досить добре. В основному:
Це робить робочий, але часто досить поганий кросворд. Я вніс декілька змін, які я вніс до базового рецепту вище, щоб досягти кращого результату.
Я нещодавно написав своє в Python. Ви можете знайти його тут: http://bryanhelmig.com/python-crossword-puzzle-generator/ . Це не створює щільних кросвордів у стилі Нью-Йорк, але стиль кросвордів, які ви можете знайти у дитячій головоломці.
На відміну від декількох алгоритмів, які я там знайшов, що реалізував випадковий брутальний метод розміщення слів, як кілька запропонованих, я спробував реалізувати трохи розумніший підхід із грубою силою при розміщенні слова. Ось мій процес:
Зрештою, у вас є пристойний кросворд або пошук слова, оскільки вони приблизно однакові. Це, як правило, працює досить добре, але повідомте мене, якщо у вас є якісь пропозиції щодо вдосконалення. Більші сітки працюють експоненціально повільніше; більший перелік слів лінійно. Більші списки слів також мають набагато більший шанс на кращу кількість слов.
array.sort(key=f)
стабільний, що означає (наприклад), що просто сортування алфавітного списку слів за довжиною зберігало б усі 8-літерні слова в алфавітному порядку.
Я фактично написав програму генерування кросвордів близько десяти років тому (це було виразно, але ті ж правила застосовуватимуться і до звичайних кросвордів).
У ньому був список слів (та пов’язаних із ними підказок), що зберігаються у файлі, відсортованому за убуванням до цього часу (так, що слова, які використовуються менше, були у верхній частині файлу). Шаблон, в основному, бітова маска, що представляє чорні та вільні квадрати, вибирався випадковим чином із пулу, який надав клієнт.
Потім для кожного неповного слова в головоломці (в основному знайдіть перший порожній квадрат і подивіться, чи є той, що праворуч (через слово) або той, що знаходиться внизу (вниз), також порожній), було проведено пошук файл шукає перше слово, яке підходить, з урахуванням букв, які вже є в цьому слові. Якщо не було слова, яке могло б підійти, ви просто позначили це слово як неповне та продовжували рух.
Зрештою, буде кілька незавершених слів, які компілятор повинен був би заповнити (та додати слово та підказку до файлу за бажанням). Якщо вони не змогли придумати жодних ідей, вони могли б відредагувати кросворд вручну, щоб змінити обмеження або просто попросити загальну регенерацію.
Після того, як файл слова / підказки набув певного розміру (і він додавав 50-100 підказок на день для цього клієнта), рідко траплялося більше двох-трьох ручних виправлень, які потрібно було зробити для кожної кросворду .
Цей алгоритм створює 50 щільних кросвордів 6х9 стрілок за 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))
Для кожного ще порожнього слота слова (який має перехрестя з уже заповненими слотами), обчисліть коефіцієнт обмеження (це може змінюватись, що просто на кількість доступних рішень на цьому кроці) та сортуйте порожні слова, за цим співвідношенням (складність O (складність NlogN ) або O (N))
Проведіть цикл порожніх наборів слів, обчислених на попередньому кроці, і для кожного спробуйте декілька рішень канкдидату (переконайтесь, що "дуга-консистенція збережена", тобто сітка має рішення після цього кроку, якщо це слово використовується) та сортуйте їх відповідно до максимальна доступність для наступного кроку (тобто наступний крок має максимально можливі рішення, якщо це слово в цей час використовується в цьому місці тощо) (складність O (N * MaxCandidatesUsed))
Заповніть це слово (позначте його як заповнене та перейдіть до кроку 2)
Якщо не знайдено жодного слова, яке б відповідало критеріям кроку .3, спробуйте повернутись до іншого рішення кандидата якогось попереднього кроку (критерії можуть змінюватись тут) (складність O (N))
Якщо зворотний трек знайдений, використовуйте альтернативну функцію та необов’язково скиньте всі вже заповнені слова, які можуть знадобитися скинути (позначте їх як знову незаповнені) (складність O (N))
Якщо зворотний шлях не знайдено, рішення не може бути знайдено (принаймні, з цією конфігурацією, початковим насінням тощо)
Інакше, коли всі слова слова заповнені, у вас є одне рішення
Цей алгоритм робить випадковим послідовним кроком дерево рішення проблеми. Якщо в якийсь момент є глухий кут, він робить зворотній шлях до попереднього вузла і слідує іншому маршруту. Доки не знайдене рішення або кількість кандидатів у різних вузлах не вичерпано.
Частина узгодженості гарантує, що знайдене рішення справді є рішенням, а випадкова частина дає змогу виробляти різні рішення в різних виконаннях, а також в середньому мати кращі показники.
PS. все це (та інші) були реалізовані у чистому JavaScript (з паралельною обробкою та WYSIWYG)
PS2. Алгоритм можна легко паралелізувати, щоб отримати одночасно більше (різних) рішення
Сподіваюся, це допомагає
Чому б просто не використати випадковий імовірнісний підхід для початку. Почніть зі слова, а потім кілька разів вибирайте випадкове слово і намагайтеся вписати його у поточний стан головоломки, не порушуючи обмежень щодо розміру тощо. Якщо ви не вдається, просто починайте все спочатку.
Ви здивуєтеся, як часто такий підхід Монте-Карло працює.
Ось код JavaScript на основі відповіді nickf та коду Брайана 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 відповідно), тоді поставте його у чергу та перевірте наступне слово.
Промийте та повторіть, і це повинно створити кросворд.
Звичайно, я майже впевнений, що це O (n!), І кросворд для вас не гарантовано, але, можливо, хтось може його вдосконалити.
Я думав над цією проблемою. Я розумію, що для створення по-справжньому щільного кросворду ви не можете сподіватися, що вашого обмеженого списку слів буде достатньо. Тому ви можете взяти словник і помістити його в структуру даних "трие". Це дозволить вам легко знайти слова, які заповнюють ліві місця пробілами. У трійці досить ефективно здійснити обхід, який, скажімо, дає всі слова форми "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:][:]
... і повторіть знову новий кросворд.
Створіть систему балів за легкістю заповнення та деякими калькуляціями. Дайте бал за поточний кросворд і звужте пізній вибір, додавши його до списку зроблених кросвордів, якщо бал буде задоволений вашою системою балів.
Після першого ітераційного сеансу повторіть перелік зі списку зроблених кросвордів, щоб закінчити завдання.
Використовуючи більше параметрів, швидкість може бути покращена величезним фактором.
Я отримав би індекс кожної літери, що використовується кожним словом, щоб знати можливі хрести. Тоді я б вибрав найбільше слово і використав би його як базу. Виберіть наступний великий і перекресліть його. Промийте і повторіть. Це, мабуть, проблема НП.
Ще одна ідея - створення генетичного алгоритму, де показник сили - це скільки слів, які ви можете помістити в сітку.
Найважча частина, яку я знаходжу, - це коли знати певний список, можливо, не можна перекреслити.
Цей з'являється як проект у курсі AI CS50 з Гарварду. Ідея полягає в тому, щоб сформулювати проблему створення кросвордів як проблему задоволення обмежень і вирішити її за допомогою зворотного відстеження з різною евристикою, щоб зменшити простір пошуку.
Для початку нам потрібно пара вхідних файлів:
`
###_####_#
____####_#
_##_#_____
_##_#_##_#
______####
#_###_####
#_##______
#_###_##_#
_____###_#
#_######_#
##_______#
`
Вхідний словник (список слів / словник), з якого будуть обрані слова-кандидати (як показано нижче).
a
abandon
ability
able
abortion
about
above
abroad
absence
absolute
absolutely
...
Тепер ДСП визначається і вирішується так:
Далі показано результат, отриманий за допомогою реалізації алгоритму вирішення 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█
`
Наступна анімація показує етапи зворотного відстеження:
Ось ще один зі списком слів на мові Bangla (бенгальська):
Я зашифрував рішення цієї проблеми в JavaScript / jQuery:
Приклад демонстрації: http://www.earthfluent.com/crossword-puzzle-demo.html
Вихідний код: https://github.com/HoldOffHunger/jquery-crossword-puzzle-generator
Завдання алгоритму, який я використав:
Я опишу використаний алгоритм:
Згрупуйте слова разом за тими, що мають спільну букву.
З цих груп побудуйте набори нової структури даних ("слова слова"), що є первинним словом (яке проходить через усі інші слова), а потім іншими словами (які проходять через первинне слово).
Починайте кросворд із самого першого з цих блоків слів у самому верхньому лівому місці кросворду.
Для решти блоків слів, починаючи з правої нижньої частини більшості кросвордів, рухайтесь вгору та вліво, поки не буде доступних проміжків для заповнення. Якщо вгору більше порожніх стовпців, ніж ліворуч, рухайтеся вгору і навпаки.
var crosswords = generateCrosswordBlockSources(puzzlewords);
. Просто консоліруйте це значення. Не забувайте, що в грі є "режим чіт", де ви можете просто натиснути "Розкрити відповідь", щоб отримати значення негайно.