Respuestas:
Se me ocurrió una solución que probablemente no sea la más eficiente, pero funciona lo suficientemente bien. Básicamente:
Esto hace un crucigrama funcional, pero a menudo bastante pobre. Hubo una serie de modificaciones que hice a la receta básica anterior para obtener un mejor resultado.
Recientemente escribí el mío en Python. Puede encontrarlo aquí: http://bryanhelmig.com/python-crossword-puzzle-generator/ . No crea los densos crucigramas de estilo NYT, sino el estilo de crucigramas que puede encontrar en el libro de rompecabezas de un niño.
A diferencia de algunos algoritmos que descubrí que implementaban un método aleatorio de fuerza bruta para colocar palabras como algunos han sugerido, traté de implementar un enfoque de fuerza bruta ligeramente más inteligente en la colocación de palabras. Aquí está mi proceso:
Al final, tienes un crucigrama decente o un rompecabezas de búsqueda de palabras, ya que son casi lo mismo. Suele funcionar bastante bien, pero avíseme si tiene alguna sugerencia sobre mejoras. Las cuadrículas más grandes corren exponencialmente más lento; listas de palabras más grandes linealmente. Las listas de palabras más grandes también tienen muchas más posibilidades de obtener mejores números de colocación de palabras.
array.sort(key=f)
es estable, lo que significa (por ejemplo) que simplemente ordenar una lista de palabras alfabéticas por longitud mantendría todas las palabras de 8 letras ordenadas alfabéticamente.
De hecho, escribí un programa de generación de crucigramas hace unos diez años (era críptico, pero las mismas reglas se aplicarían para los crucigramas normales).
Tenía una lista de palabras (y pistas asociadas) almacenadas en un archivo ordenado por uso descendente hasta la fecha (de modo que las palabras menos utilizadas estaban en la parte superior del archivo). Se eligió al azar una plantilla, básicamente una máscara de bits que representa los cuadrados negros y libres, de un grupo proporcionado por el cliente.
Luego, para cada palabra no completa en el rompecabezas (básicamente encuentre el primer cuadrado en blanco y vea si el que está a la derecha (palabra cruzada) o el que está debajo (palabra abajo) también está en blanco), se realizó una búsqueda de el archivo busca la primera palabra que encaja, teniendo en cuenta las letras que ya están en esa palabra. Si no había una palabra que pudiera encajar, simplemente marcó la palabra completa como incompleta y siguió adelante.
Al final habría algunas palabras incompletas que el compilador tendría que completar (y agregar la palabra y una pista al archivo si lo desea). Si no se les ocurriera ninguna idea, podrían editar el crucigrama manualmente para cambiar las restricciones o simplemente pedir una regeneración total.
Una vez que el archivo de palabra / pista alcanzó un cierto tamaño (y agregaba entre 50 y 100 pistas por día para este cliente), rara vez hubo que hacer más de dos o tres arreglos manuales para cada crucigrama. .
Este algoritmo crea 50 crucigramas densos de flecha 6x9 en 60 segundos. Utiliza una base de datos de palabras (con palabras + consejos) y una base de datos de placas (con placas preconfiguradas).
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
¡Una base de datos de palabras más grande disminuye considerablemente el tiempo de generación y algunos tipos de tableros son más difíciles de llenar! ¡Las tablas más grandes requieren más tiempo para llenarse correctamente!
Ejemplo:
Tarjeta 6x9 preconfigurada:
(# significa una punta en una celda,% significa dos puntas en una celda, no se muestran flechas)
# - # # - % # - #
- - - - - - - - -
# - - - - - # - -
% - - # - # - - -
% - - - - - % - -
- - - - - - - - -
Tablero 6x9 generado:
# 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
Consejos [línea, columna]:
[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)
Aunque esta es una pregunta anterior, intentaré una respuesta basada en un trabajo similar que he realizado.
Hay muchos enfoques para resolver problemas de restricción (que generallay están en la clase de complejidad NPC).
Esto está relacionado con la optimización combinatoria y la programación de restricciones. En este caso, las restricciones son la geometría de la cuadrícula y el requisito de que las palabras sean únicas, etc.
Los enfoques de aleatorización / recocido también pueden funcionar (aunque dentro del entorno adecuado).
¡La simplicidad eficiente podría ser la sabiduría suprema!
Los requisitos eran para un compilador de crucigramas más o menos completo y un generador (visual WYSIWYG).
Dejando a un lado la parte del constructor WYSIWYG, el resumen del compilador fue este:
Cargue las listas de palabras disponibles (ordenadas por longitud de palabra, es decir, 2,3, .., 20)
Encuentre los espacios de palabras (es decir, palabras de la cuadrícula) en la cuadrícula construida por el usuario (por ejemplo, palabra en x, y con longitud L, horizontal o vertical) (complejidad O (N))
Calcule los puntos de intersección de las palabras de la cuadrícula (que deben completarse) (complejidad O (N ^ 2))
Calcule las intersecciones de las palabras en las listas de palabras con las distintas letras del alfabeto utilizadas (esto permite buscar palabras coincidentes mediante el uso de una plantilla, por ejemplo, la tesis de Sik Cambon como la utilizada por cwc ) (complejidad O (WL * AL))
Los pasos .3 y .4 permiten hacer esta tarea:
a. Las intersecciones de las palabras de la cuadrícula con ellas mismas permiten crear una "plantilla" para tratar de encontrar coincidencias en la lista de palabras asociadas de palabras disponibles para esta palabra de la cuadrícula (mediante el uso de las letras de otras palabras de intersección con esta palabra que ya están llenas en un cierto paso del algoritmo)
si. Las intersecciones de las palabras en una lista de palabras con el alfabeto permiten encontrar palabras coincidentes (candidatas) que coinciden con una "plantilla" dada (por ejemplo, 'A' en primer lugar y 'B' en tercer lugar, etc.)
Entonces, con estas estructuras de datos implementadas, el algoritmo utilizado fue algo así:
NOTA: si la cuadrícula y la base de datos de palabras son constantes, los pasos anteriores solo se pueden hacer una vez.
El primer paso del algoritmo es seleccionar un espacio de palabras vacío (palabra de cuadrícula) al azar y llenarlo con una palabra candidata de su lista de palabras asociada (la aleatorización permite producir diferentes soluciones en ejecuciones consecutivas del algoritmo) (complejidad O (1) u O ( N))
Para cada espacio de palabras aún vacío (que tiene intersecciones con espacios de palabras ya llenos), calcule una razón de restricción (esto puede variar, algo simple es el número de soluciones disponibles en ese paso) y clasifique los espacios de palabras vacíos por esta razón (complejidad O (NlogN ) o O (N))
Recorra los espacios de palabras vacíos calculados en el paso anterior y para cada uno intente una serie de soluciones cancdidadas (asegurándose de que "se conserve la consistencia del arco", es decir, la cuadrícula tiene una solución después de este paso si se usa esta palabra) y ordénelas de acuerdo con disponibilidad máxima para el siguiente paso (es decir, el siguiente paso tiene el máximo de soluciones posibles si esta palabra se usa en ese momento en ese lugar, etc.) (complejidad O (N * MaxCandidatesUsed))
Rellene esa palabra (márquela como rellenada y vaya al paso 2)
Si no se encuentra una palabra que satisfaga los criterios del paso .3, intente retroceder a otra solución candidata de algún paso anterior (los criterios pueden variar aquí) (complejidad O (N))
Si se encuentra un retroceso, use la alternativa y, opcionalmente, restablezca las palabras ya rellenadas que puedan necesitar restablecerse (márquelas como no rellenadas nuevamente) (complejidad O (N))
Si no se encuentra un retroceso, no se puede encontrar la solución (al menos con esta configuración, inicialización, etc.)
De lo contrario, cuando todos los lotes de palabras están llenos, tienes una solución
Este algoritmo realiza un recorrido aleatorio consistente del árbol de solución del problema. Si en algún momento hay un callejón sin salida, realiza un retroceso a un nodo anterior y sigue otra ruta. Hasta que se encuentre una solución encontrada o se agote el número de candidatos para los distintos nodos.
La parte de coherencia asegura que una solución encontrada sea realmente una solución y la parte aleatoria permite producir diferentes soluciones en diferentes ejecuciones y también, en promedio, tener un mejor rendimiento.
PD. todo esto (y otros) se implementaron en la capacidad de JavaScript puro (con procesamiento paralelo y WYSIWYG)
PS2 El algoritmo se puede paralelizar fácilmente para producir más de una solución (diferente) al mismo tiempo
Espero que esto ayude
¿Por qué no utilizar un enfoque probabilístico aleatorio para comenzar? Comience con una palabra, y luego elija repetidamente una palabra al azar e intente ajustarla al estado actual del rompecabezas sin romper las restricciones en el tamaño, etc. Si falla, simplemente comience de nuevo.
Te sorprenderá la frecuencia con la que funciona un enfoque de Monte Carlo como este.
Aquí hay un código JavaScript basado en la respuesta de nickf y el código Python de Bryan. Solo publícalo en caso de que alguien más lo necesite en 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();
}
Generaría dos números: longitud y puntuación de Scrabble. Suponga que un puntaje bajo de Scrabble significa que es más fácil unirse (puntajes bajos = muchas letras comunes). Ordene la lista por longitud descendente y puntuación de Scrabble ascendente.
A continuación, solo baja la lista. Si la palabra no se cruza con una palabra existente (verifique cada palabra por su longitud y puntaje de Scrabble, respectivamente), luego póngala en la cola y verifique la siguiente palabra.
Enjuague y repita, y esto debería generar un crucigrama.
Por supuesto, estoy bastante seguro de que esto es O (n!) Y no se garantiza que complete el crucigrama por usted, pero tal vez alguien pueda mejorarlo.
He estado pensando en este problema. Mi sensación es que para crear un crucigrama realmente denso, no puede esperar que su lista de palabras limitada sea suficiente. Por lo tanto, es posible que desee tomar un diccionario y colocarlo en una estructura de datos "trie". Esto le permitirá encontrar fácilmente palabras que llenen los espacios sobrantes. En un trie, es bastante eficiente implementar un recorrido que, por ejemplo, le da todas las palabras de la forma "c? T".
Por lo tanto, mi pensamiento general es: crear algún tipo de enfoque de fuerza bruta como se describe aquí para crear una cruz de baja densidad y completar los espacios en blanco con palabras del diccionario.
Si alguien más ha adoptado este enfoque, hágamelo saber.
Estaba jugando con el motor generador de crucigramas, y esto me pareció lo más importante:
0.!/usr/bin/python
a. allwords.sort(key=len, reverse=True)
si. haga algún elemento / objeto como el cursor que caminará alrededor de la matriz para una fácil orientación a menos que desee iterar por elección aleatoria más adelante.
el primero, toma el primer par y colócalos a través de 0,0; almacenar el primero como nuestro crucigrama actual 'líder'.
mover el cursor por orden diagonal o aleatorio con mayor probabilidad diagonal a la siguiente celda vacía
iterar sobre las palabras me gusta y usar la longitud del espacio libre para definir la longitud máxima de la palabra:
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 )
para comparar la palabra con el espacio libre que usé, es decir:
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
Después de cada palabra utilizada con éxito, cambie de dirección. Bucle mientras se llenan todas las celdas O se queda sin palabras O por límite de iteraciones y luego:
# CHANGE ALL WORDS LIST
inexOf1stWord = allwords.index( leading_w )
allwords = allwords[:inexOf1stWord+1][:] + allwords[inexOf1stWord+1:][:]
... e iterar de nuevo nuevo crucigrama.
Haga el sistema de puntuación por facilidad de llenado y algunos cálculos de estimación. Proporcione puntaje para el crucigrama actual y limite la opción posterior agregándolo a la lista de crucigramas hechos si el sistema de puntaje satisface el puntaje.
Después de la primera sesión de iteración, repita nuevamente desde la lista de crucigramas hechos para finalizar el trabajo.
Mediante el uso de más parámetros, la velocidad puede mejorarse en gran medida.
Obtendría un índice de cada letra utilizada por cada palabra para conocer posibles cruces. Entonces elegiría la palabra más grande y la usaría como base. Seleccione el próximo grande y crúcelo. Enjuague y repita. Probablemente sea un problema de NP.
Otra idea es crear un algoritmo genético donde la métrica de la fuerza es cuántas palabras puedes poner en la cuadrícula.
Lo difícil que encuentro es cuándo no se puede cruzar una determinada lista.
He codificado una solución JavaScript / jQuery para este problema:
Demostración de muestra: http://www.earthfluent.com/crossword-puzzle-demo.html
Código fuente: https://github.com/HoldOffHunger/jquery-crossword-puzzle-generator
La intención del algoritmo que utilicé:
Describiré el algoritmo que utilicé:
Agrupe las palabras de acuerdo con las que comparten una letra común.
A partir de estos grupos, cree conjuntos de una nueva estructura de datos ("bloques de palabras"), que es una palabra principal (que se ejecuta a través de todas las demás palabras) y luego las otras palabras (que se ejecutan a través de la palabra principal).
Comience el crucigrama con el primero de estos bloques de palabras en la posición superior izquierda del crucigrama.
Para el resto de los bloques de palabras, comenzando desde la posición más inferior derecha del crucigrama, muévase hacia arriba y hacia la izquierda, hasta que no haya más espacios disponibles para llenar. Si hay más columnas vacías hacia arriba que hacia la izquierda, muévase hacia arriba y viceversa.
var crosswords = generateCrosswordBlockSources(puzzlewords);
. Solo consola registre este valor. No olvides que hay un "modo trampa" en el juego, en el que puedes hacer clic en "Revelar respuesta" para obtener el valor de inmediato.
Este aparece como un proyecto en el curso AI CS50 de Harvard. La idea es formular el problema de generación de crucigramas como un problema de satisfacción de restricciones y resolverlo con retroceso con diferentes heurísticas para reducir el espacio de búsqueda.
Para comenzar necesitamos un par de archivos de entrada:
``
###_####_#
____####_#
_##_#_____
_##_#_##_#
______####
#_###_####
#_##______
#_###_##_#
_____###_#
#_######_#
##_______#
``
Un vocabulario de entrada (lista de palabras / diccionario) del que se elegirán las palabras candidatas (como la que se muestra a continuación).
a
abandon
ability
able
abortion
about
above
abroad
absence
absolute
absolutely
...
Ahora el CSP está definido y se resolverá de la siguiente manera:
A continuación se muestra el resultado que se obtuvo utilizando una implementación del algoritmo de resolución 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 siguiente animación muestra los pasos de retroceso:
Aquí hay otro con una lista de palabras en idioma bengalí (bengalí):