Risposte:
Ho trovato una soluzione che probabilmente non è la più efficiente, ma funziona abbastanza bene. Fondamentalmente:
Questo è un cruciverba funzionante, ma spesso piuttosto scadente. Ci sono state una serie di modifiche che ho apportato alla ricetta di base sopra per ottenere un risultato migliore.
Di recente ho scritto il mio in Python. Puoi trovarlo qui: http://bryanhelmig.com/python-crossword-puzzle-generator/ . Non crea i densi cruciverba in stile NYT, ma lo stile dei cruciverba che potresti trovare nel libro di puzzle di un bambino.
A differenza di alcuni algoritmi che ho scoperto che implementavano un metodo di forza bruta casuale per posizionare le parole come alcuni hanno suggerito, ho cercato di implementare un approccio di forza bruta leggermente più intelligente al posizionamento delle parole. Ecco il mio processo:
Alla fine, hai un cruciverba decente o un puzzle di ricerca di parole, dal momento che sono più o meno gli stessi. Tende a funzionare piuttosto bene, ma fammi sapere se hai suggerimenti per migliorare. Le griglie più grandi funzionano esponenzialmente più lentamente; elenchi di parole più grandi in modo lineare. Gli elenchi di parole più grandi hanno anche una probabilità molto maggiore di ottenere numeri di posizionamento delle parole migliori.
array.sort(key=f)
è stabile, il che significa (ad esempio) che semplicemente ordinare un elenco di parole alfabetico per lunghezza manterrebbe tutte le parole di 8 lettere ordinate alfabeticamente.
In realtà ho scritto un programma per la generazione di cruciverba circa dieci anni fa (era criptico ma le stesse regole si applicano ai normali cruciverba).
Aveva un elenco di parole (e indizi associati) memorizzati in un file ordinato in base all'uso decrescente fino ad oggi (in modo che le parole meno usate fossero all'inizio del file). Un modello, fondamentalmente una maschera di bit che rappresenta i quadrati neri e liberi, è stato scelto a caso da un pool fornito dal cliente.
Quindi, per ogni parola non completa nel puzzle (fondamentalmente trova il primo quadrato vuoto e vedi se anche quello a destra (parola trasversale) o quello sotto (parola in basso) è vuoto), è stata eseguita una ricerca di il file cercando la prima parola che si adattava, tenendo conto delle lettere già presenti in quella parola. Se non c'era una parola adatta, hai semplicemente contrassegnato l'intera parola come incompleta e sei andato avanti.
Alla fine ci sarebbero alcune parole non completate che il compilatore dovrebbe compilare (e aggiungere la parola e un indizio al file se lo desidera). Se non potevano venire con eventuali idee, potrebbero modificare il cruciverba manualmente per modificare i vincoli o semplicemente chiedere per un totale ri-generazione.
Una volta che il file di parole / indizi raggiungeva una certa dimensione (e aggiungeva 50-100 indizi al giorno per questo client), raramente si verificava più di due o tre correzioni manuali per ogni cruciverba .
Questo algoritmo crea 50 parole crociate con frecce 6x9 in 60 secondi. Utilizza un database di parole (con parole + suggerimenti) e un database di schede (con schede preconfigurate).
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
Un database di parole più grande riduce notevolmente il tempo di generazione e alcuni tipi di schede sono più difficili da riempire! Le schede più grandi richiedono più tempo per essere riempite correttamente!
Esempio:
Scheda 6x9 preconfigurata:
(# significa un suggerimento in una cella,% significa due suggerimenti in una cella, frecce non mostrate)
# - # # - % # - #
- - - - - - - - -
# - - - - - # - -
% - - # - # - - -
% - - - - - % - -
- - - - - - - - -
Scheda 6x9 generata:
# 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
Suggerimenti [riga, colonna]:
[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)
Sebbene questa sia una domanda più vecchia, tenterò una risposta basata su un lavoro simile che ho fatto.
Esistono molti approcci per risolvere i problemi di vincolo (che in generale sono nella classe di complessità NPC).
Ciò è correlato all'ottimizzazione combinatoria e alla programmazione con vincoli. In questo caso i vincoli sono la geometria della griglia e il requisito che le parole siano uniche ecc.
Anche gli approcci di randomizzazione / ricottura possono funzionare (sebbene all'interno dell'impostazione corretta).
La semplicità efficiente potrebbe essere solo l'ultima saggezza!
I requisiti erano per un compilatore di parole crociate più o meno completo e un builder (visual WYSIWYG).
Lasciando da parte la parte del costruttore WYSIWYG, lo schema del compilatore era questo:
Carica le liste di parole disponibili (ordinate per lunghezza della parola, cioè 2,3, .., 20)
Trova le aree di parole (cioè le parole della griglia) sulla griglia costruita dall'utente (es. Parola in x, y con lunghezza L, orizzontale o verticale) (complessità O (N))
Calcola i punti di intersezione delle parole della griglia (che devono essere riempite) (complessità O (N ^ 2))
Calcola le intersezioni delle parole nelle liste di parole con le varie lettere dell'alfabeto utilizzato (questo permette di cercare parole corrispondenti utilizzando un modello es. Tesi Sik Cambon come usato da cwc ) (complessità O (WL * AL))
I passaggi .3 e .4 consentono di eseguire questa operazione:
un. Le intersezioni delle parole della griglia con se stesse consentono di creare un "modello" per cercare di trovare corrispondenze nell'elenco di parole associate di parole disponibili per questa parola della griglia (utilizzando le lettere di altre parole che si intersecano con questa parola che sono già riempite in un certo passo dell'algoritmo)
b. Le intersezioni delle parole in un elenco di parole con l'alfabeto consentono di trovare parole corrispondenti (candidate) che corrispondono a un dato "modello" (es. 'A' al 1 ° posto e 'B' al 3 ° posto ecc ..)
Quindi, con queste strutture dati implementate, l'algoritmo utilizzato era qc come questo:
NOTA: se la griglia e il database delle parole sono costanti, i passaggi precedenti possono essere eseguiti una sola volta.
Il primo passo dell'algoritmo è selezionare uno slot di parole vuoto (parola della griglia) a caso e riempirlo con una parola candidata dalla sua lista di parole associata (la randomizzazione consente di produrre diverse soluzioni in esecuzioni consecutive dell'algoritmo) (complessità O (1) o O ( N))
Per ogni slot di parola ancora vuoto (che ha intersezioni con spazi di parole già riempiti), calcola un rapporto di vincolo (questo può variare, sth semplice è il numero di soluzioni disponibili in quel passaggio) e ordina gli slot di parole vuoti in base a questo rapporto (complessità O (NlogN ) o O (N))
Scorri le fasce di parole vuote calcolate nel passaggio precedente e per ognuna prova un numero di soluzioni cancdidate (assicurandoti che "la consistenza dell'arco sia mantenuta", cioè la griglia ha una soluzione dopo questo passaggio se questa parola è usata) e ordinale in base a massima disponibilità per il passaggio successivo (ovvero il passaggio successivo ha il massimo delle soluzioni possibili se questa parola viene utilizzata in quel momento in quel luogo, ecc.) (complessità O (N * MaxCandidatesUsed))
Compila quella parola (contrassegnala come riempita e vai al passaggio 2)
Se non viene trovata alcuna parola che soddisfi i criteri del passaggio .3, provare a tornare indietro a un'altra soluzione candidata di qualche passaggio precedente (i criteri possono variare qui) (complessità O (N))
Se viene trovato il backtrack, usa l'alternativa e facoltativamente reimposta tutte le parole già riempite che potrebbero dover essere ripristinate (contrassegnale di nuovo come non riempite) (complessità O (N))
Se non viene trovato alcun backtrack, la soluzione no può essere trovata (almeno con questa configurazione, seed iniziale ecc ..)
Altrimenti quando tutti i wordlots sono riempiti hai una soluzione
Questo algoritmo esegue una passeggiata coerente casuale dell'albero delle soluzioni del problema. Se a un certo punto c'è un vicolo cieco, torna a un nodo precedente e segue un altro percorso. Fino ad esaurimento di una soluzione trovata o del numero di candidati per i vari nodi.
La parte di consistenza fa in modo che una soluzione trovata sia effettivamente una soluzione e la parte casuale consente di produrre diverse soluzioni in diverse esecuzioni e anche mediamente avere prestazioni migliori.
PS. tutto questo (e altri) sono stati implementati in puro JavaScript (con elaborazione parallela e WYSIWYG)
PS2. L'algoritmo può essere facilmente parallelizzato per produrre più di una (diversa) soluzione allo stesso tempo
Spero che questo ti aiuti
Perché non utilizzare semplicemente un approccio probabilistico casuale per iniziare. Inizia con una parola, quindi scegli ripetutamente una parola a caso e cerca di adattarla allo stato corrente del puzzle senza infrangere i vincoli sulla dimensione, ecc. Se fallisci, ricomincia da capo.
Sarai sorpreso di quanto spesso funzioni un approccio Monte Carlo come questo.
Ecco del codice JavaScript basato sulla risposta di nickf e sul codice Python di Bryan. Basta pubblicarlo nel caso in cui qualcun altro ne abbia bisogno in 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();
}
Genererei due numeri: lunghezza e punteggio di Scrabble. Supponiamo che un punteggio di Scrabble basso significhi che è più facile partecipare (punteggi bassi = molte lettere comuni). Ordina l'elenco in base alla lunghezza decrescente e al punteggio di Scrabble crescente.
Quindi, vai in fondo alla lista. Se la parola non si incrocia con una parola esistente (controlla ogni parola in base alla lunghezza e al punteggio di Scrabble, rispettivamente), mettila in coda e controlla la parola successiva.
Risciacquare e ripetere, e questo dovrebbe generare un cruciverba.
Certo, sono abbastanza sicuro che questo sia O (n!) E non è garantito che completi il cruciverba per te, ma forse qualcuno può migliorarlo.
Ho pensato a questo problema. La mia sensazione è che per creare un cruciverba davvero denso, non puoi sperare che il tuo elenco limitato di parole sia sufficiente. Pertanto, potresti prendere un dizionario e inserirlo in una struttura dati "trie". Ciò ti consentirà di trovare facilmente le parole che riempiono gli spazi rimasti. In un trie, è abbastanza efficiente implementare un attraversamento che, diciamo, ti dà tutte le parole della forma "c? T".
Quindi, il mio pensiero generale è: creare una sorta di approccio di forza relativamente bruta come alcuni descritti qui per creare una croce a bassa densità e riempire gli spazi vuoti con le parole del dizionario.
Se qualcun altro ha adottato questo approccio, fatemelo sapere.
Stavo giocando con il generatore di cruciverba e ho trovato questo il più importante:
0.!/usr/bin/python
un. allwords.sort(key=len, reverse=True)
b. crea un oggetto / oggetto come un cursore che camminerà intorno alla matrice per un facile orientamento a meno che tu non voglia iterare per scelta casuale in seguito.
il primo, prendi il primo paio e mettilo di traverso e in basso da 0,0; memorizzare il primo come il nostro cruciverba corrente "leader".
sposta il cursore in ordine diagonale o casuale con maggiore probabilità diagonale alla successiva cella vuota
itera sulle parole come e usa la lunghezza dello spazio libero per definire la lunghezza massima della parola:
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 )
per confrontare la parola con lo spazio libero che ho usato, ad esempio:
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
dopo ogni parola usata con successo, cambia direzione. Ripeti il ciclo mentre tutte le celle sono piene O finisci le parole O per il limite di iterazioni, quindi:
# CHANGE ALL WORDS LIST
inexOf1stWord = allwords.index( leading_w )
allwords = allwords[:inexOf1stWord+1][:] + allwords[inexOf1stWord+1:][:]
... e ripeti un nuovo cruciverba.
Crea il sistema di punteggio con facilità di riempimento e alcuni calcoli di stima. Assegna un punteggio al cruciverba corrente e restringi la scelta successiva aggiungendolo all'elenco dei cruciverba creati se il punteggio è soddisfatto dal tuo sistema di punteggio.
Dopo la prima sessione di iterazione, iterare di nuovo dall'elenco dei cruciverba creati per completare il lavoro.
Utilizzando più parametri, la velocità può essere migliorata di un fattore enorme.
Otterrei un indice di ogni lettera usata da ogni parola per conoscere le possibili croci. Quindi sceglierei la parola più grande e la userei come base. Seleziona il prossimo grande e attraversalo. Risciacqua e ripeti. Probabilmente è un problema NP.
Un'altra idea è creare un algoritmo genetico in cui la metrica della forza è quante parole puoi inserire nella griglia.
La parte difficile che trovo è quando sapere che un certo elenco non può essere superato.
Questo appare come un progetto nel corso AI CS50 di Harvard. L'idea è di formulare il problema della generazione di cruciverba come un problema di soddisfazione dei vincoli e risolverlo con il backtracking con diverse euristiche per ridurre lo spazio di ricerca.
Per iniziare abbiamo bisogno di un paio di file di input:
`
###_####_#
____####_#
_##_#_____
_##_#_##_#
______####
#_###_####
#_##______
#_###_##_#
_____###_#
#_######_#
##_______#
`
Un vocabolario di input (elenco di parole / dizionario) da cui verranno scelte le parole candidate (come quella mostrata di seguito).
a
abandon
ability
able
abortion
about
above
abroad
absence
absolute
absolutely
...
Ora il CSP è definito e da risolvere come segue:
Di seguito è mostrato l'output ottenuto utilizzando un'implementazione dell'algoritmo di risoluzione 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█
`
La seguente animazione mostra i passaggi del backtracking:
Eccone un altro con un elenco di parole in lingua bengalese:
Ho codificato una soluzione JavaScript / jQuery a questo problema:
Demo di esempio: http://www.earthfluent.com/crossword-puzzle-demo.html
Codice sorgente: https://github.com/HoldOffHunger/jquery-crossword-puzzle-generator
L'intento dell'algoritmo che ho usato:
Descriverò l'algoritmo che ho usato:
Raggruppa le parole in base a quelle che condividono una lettera comune.
Da questi gruppi, crea insiemi di una nuova struttura dati ("blocchi di parole"), che è una parola primaria (che attraversa tutte le altre parole) e poi le altre parole (che attraversano la parola primaria).
Inizia il cruciverba con il primo di questi blocchi di parole nella posizione in alto a sinistra del cruciverba.
Per il resto dei blocchi di parole, partendo dalla posizione più in basso a destra del cruciverba, spostati verso l'alto e verso sinistra, finché non ci sono più spazi disponibili da riempire. Se ci sono più colonne vuote verso l'alto che verso sinistra, spostati verso l'alto e viceversa.
var crosswords = generateCrosswordBlockSources(puzzlewords);
. Basta registrare questo valore dalla console. Non dimenticare che c'è una "modalità cheat" nel gioco, in cui puoi semplicemente fare clic su "Mostra risposta", per ottenere immediatamente il valore.