Algorithmus zum Generieren eines Kreuzworträtsels

122

Wie würden Sie bei einer Liste von Wörtern vorgehen, um sie in einem Kreuzworträtselgitter anzuordnen?

Es müsste nicht wie ein "richtiges" Kreuzworträtsel sein, das symmetrisch ist oder so etwas: Geben Sie einfach eine Startposition und -richtung für jedes Wort aus.

nickf
quelle

Antworten:

62

Ich habe eine Lösung gefunden, die wahrscheinlich nicht die effizienteste ist, aber gut genug funktioniert. Grundsätzlich:

  1. Sortieren Sie alle Wörter nach Länge und absteigend.
  2. Nimm das erste Wort und lege es an die Tafel.
  3. Nimm das nächste Wort.
  4. Durchsuchen Sie alle Wörter, die sich bereits an der Tafel befinden, und prüfen Sie, ob es mögliche Schnittpunkte (gemeinsame Buchstaben) mit diesem Wort gibt.
  5. Wenn es einen möglichen Ort für dieses Wort gibt, durchlaufen Sie alle Wörter auf der Tafel und prüfen Sie, ob das neue Wort stört.
  6. Wenn dieses Wort die Tafel nicht beschädigt, platzieren Sie es dort und fahren Sie mit Schritt 3 fort. Andernfalls suchen Sie weiter nach einem Ort (Schritt 4).
  7. Setzen Sie diese Schleife fort, bis alle Wörter entweder platziert sind oder nicht mehr platziert werden können.

Dies macht ein funktionierendes, aber oft recht schlechtes Kreuzworträtsel. Es gab eine Reihe von Änderungen, die ich am obigen Grundrezept vorgenommen habe, um ein besseres Ergebnis zu erzielen.

  • Geben Sie am Ende der Generierung eines Kreuzworträtsels eine Punktzahl an, die darauf basiert, wie viele Wörter platziert wurden (je mehr desto besser), wie groß die Tafel ist (je kleiner desto besser) und wie hoch das Verhältnis zwischen Höhe und Breite ist (je näher) zu 1 desto besser). Generieren Sie eine Reihe von Kreuzworträtseln, vergleichen Sie deren Ergebnisse und wählen Sie das beste aus.
    • Anstatt eine beliebige Anzahl von Iterationen auszuführen, habe ich beschlossen, so viele Kreuzworträtsel wie möglich in einer beliebigen Zeitspanne zu erstellen. Wenn Sie nur eine kleine Wortliste haben, erhalten Sie in 5 Sekunden Dutzende möglicher Kreuzworträtsel. Ein größeres Kreuzworträtsel kann nur aus 5-6 Möglichkeiten ausgewählt werden.
  • Wenn Sie ein neues Wort platzieren, anstatt es sofort nach dem Finden einer akzeptablen Position zu platzieren, geben Sie dieser Wortposition eine Bewertung, die darauf basiert, um wie viel das Raster vergrößert wird und wie viele Schnittpunkte es gibt (idealerweise möchten Sie, dass jedes Wort ist gekreuzt von 2-3 anderen Wörtern). Behalten Sie alle Positionen und ihre Punktzahlen im Auge und wählen Sie dann die beste aus.
nickf
quelle
7
Ich schreibe gerade dieses Programm, während wir sprechen, und dies ist der identische Algorithmus, den ich auch gewählt habe. Für eine kleine Anzahl von Wörtern (10 oder weniger) hat das Programm keine Probleme, alle möglichen Lösungen in Millisekunden zu berechnen. Der Algorithmus ist jedoch exponentiell. Der einfache Teil besteht darin, den grundlegenden Algorithmus zu schreiben, der alle möglichen Kombinationen brutal durchläuft. Der schwierige Teil sind etwa ein Dutzend Abkürzungen, die Sie benötigen, um zu verhindern, dass das Programm alle Sackgassenlösungen ausprobiert.
user31586
2
"5. ... und prüfen Sie, ob das neue Wort stört" Wie erklären Sie Situationen, in denen das neue Wort neben einem vorhandenen Wort steht, das an den Stellen, an denen es benachbarte Quadrate hat, Kauderwelsch erzeugt? Beispiel: LEMON ERASE Wenn "LE", "ER" und "MA" usw. keine Wörter in Ihrer Liste sind, ist dies falsch. Auf der anderen Seite könnte die völlige Ablehnung solcher Nachbarschaften wirklich gute Gitter wegwerfen, wie: W LEMON ERASE NEXUS TT
George Armhold
4
@Koffein, ja, ich weiß, was du meinst - ich musste diese Optionen wegwerfen, denn obwohl sie wirklich gute Gitter erzeugen könnten, ist es zu schwer zu überprüfen (lesen Sie: Ich konnte nicht gestört werden ) , und es besteht die Möglichkeit, dass es sowieso nur eine Störung ist .
Nickf
4
Erstellt mit jQuery / Javascript unter Verwendung der obigen Empfehlungen und einiger meiner eigenen ... mlewiscs.com/crossword
MLewisCodeSolutions
@MLewisCodeSolutions Sieht fantastisch aus. Sind Sie Open-Sourcing-Unternehmen?
GKS
23

Ich habe erst kürzlich meine eigene in Python geschrieben. Sie finden es hier: http://bryanhelmig.com/python-crossword-puzzle-generator/ . Es werden nicht die dichten Kreuzworträtsel im NYT-Stil erstellt, sondern die Kreuzworträtsel, die Sie möglicherweise im Puzzlespielbuch eines Kindes finden.

Im Gegensatz zu einigen Algorithmen, die ich dort herausgefunden habe und die eine zufällige Brute-Force-Methode zum Platzieren von Wörtern implementiert haben, wie einige vorgeschlagen haben, habe ich versucht, einen etwas intelligenteren Brute-Force-Ansatz bei der Wortplatzierung zu implementieren. Hier ist mein Prozess:

  1. Erstellen Sie ein Raster beliebiger Größe und eine Liste von Wörtern.
  2. Mische die Wortliste und sortiere sie dann nach der längsten bis zur kürzesten.
  3. Platzieren Sie das erste und längste Wort ganz links oben, 1,1 (vertikal oder horizontal).
  4. Fahren Sie mit dem nächsten Wort fort, durchlaufen Sie jeden Buchstaben im Wort und jede Zelle im Raster und suchen Sie nach Übereinstimmungen zwischen Buchstaben.
  5. Wenn eine Übereinstimmung gefunden wird, fügen Sie diese Position einfach einer vorgeschlagenen Koordinatenliste für dieses Wort hinzu.
  6. Durchlaufen Sie die vorgeschlagene Koordinatenliste und "bewerten" Sie die Wortplatzierung basierend auf der Anzahl der anderen Wörter, die sie kreuzt. Punktzahlen von 0 zeigen entweder eine schlechte Platzierung (neben vorhandenen Wörtern) an oder dass keine Wortkreuze vorhanden waren.
  7. Zurück zu Schritt 4, bis die Wortliste erschöpft ist. Optionaler zweiter Durchgang.
  8. Wir sollten jetzt ein Kreuzworträtsel haben, aber die Qualität kann aufgrund einiger zufälliger Platzierungen getroffen oder verfehlt werden. Also puffern wir dieses Kreuzworträtsel und kehren zu Schritt 2 zurück. Wenn das nächste Kreuzworträtsel mehr Wörter auf der Tafel hat, ersetzt es das Kreuzworträtsel im Puffer. Dies ist zeitlich begrenzt (finden Sie das beste Kreuzworträtsel in x Sekunden).

Am Ende haben Sie ein anständiges Kreuzworträtsel oder ein Wortsuchrätsel, da sie ungefähr gleich sind. Es läuft in der Regel recht gut, aber lassen Sie mich wissen, wenn Sie Verbesserungsvorschläge haben. Größere Gitter laufen exponentiell langsamer. größere Wortlisten linear. Größere Wortlisten haben auch eine viel höhere Chance auf bessere Wortplatzierungsnummern.

Bryan
quelle
@Neil N: Wahrscheinlich eine bessere Möglichkeit zur Buchstabenübereinstimmung für die anderen Wörter. Wäre vielleicht auch ein Ansatz, um nach der Anzahl der pro Wort enthaltenen Buchstaben zu sortieren, was meist zum gleichen Ergebnis führt.
Karl Adler
@NeilN Pythons array.sort(key=f)ist stabil, was (zum Beispiel) bedeutet, dass durch einfaches Sortieren einer alphabetischen Wortliste nach Länge alle 8-Buchstaben-Wörter alphabetisch sortiert bleiben.
Lynn
4
@Bryan, Ihr Website-Link funktioniert bei mir nicht und die primäre Domain leitet nur zu Twitter weiter. Haben Sie einen aktualisierten Link zu Ihrem Code?
Michael A
2
Hier ist (anscheinend) ein Klon von Bryans Generator: github.com/jeremy886/crossword_helmig
lvictorino
20

Ich habe vor ungefähr zehn Jahren tatsächlich ein Programm zur Generierung von Kreuzworträtseln geschrieben (es war kryptisch, aber für normale Kreuzworträtsel gelten dieselben Regeln).

Es hatte eine Liste von Wörtern (und zugehörigen Hinweisen) in einer Datei gespeichert, sortiert nach absteigender Verwendung bis heute (so dass weniger verwendete Wörter oben in der Datei standen). Eine Vorlage, im Grunde eine Bitmaske, die die schwarzen und freien Quadrate darstellt, wurde zufällig aus einem vom Client bereitgestellten Pool ausgewählt.

Dann wurde für jedes nicht vollständige Wort im Puzzle (im Grunde genommen das erste leere Quadrat finden und prüfen, ob das rechte (quer zum Wort) oder das darunter liegende (das untere Wort) ebenfalls leer ist) eine Suche durchgeführt Die Datei sucht nach dem ersten passenden Wort und berücksichtigt dabei die Buchstaben, die bereits in diesem Wort enthalten sind. Wenn es kein passendes Wort gab, haben Sie das ganze Wort als unvollständig markiert und sind weitergegangen.

Am Ende wären einige unvollständige Wörter, die der Compiler ausfüllen müsste (und das Wort und einen Hinweis zur Datei hinzufügen, falls gewünscht). Wenn sie keine Ideen haben, können sie das Kreuzworträtsel manuell bearbeiten, um Einschränkungen zu ändern, oder einfach eine vollständige Neuerstellung anfordern.

Sobald die Wort- / Hinweisdatei eine bestimmte Größe erreicht hatte (und für diesen Client täglich 50 bis 100 Hinweise hinzugefügt wurden), gab es selten mehr als zwei oder drei manuelle Korrekturen, die für jedes Kreuzworträtsel durchgeführt werden mussten .

paxdiablo
quelle
Dies hilft mir in meiner Situation nicht wirklich, da ich eine Liste mit nur 6-12 Wörtern habe. Meins ist eher eine Lernübung für den Benutzer als ein Worträtsel. +1 für den interessanten Algorithmus trotzdem!
Nickf
1
Schöne Beschreibung. Ich habe in der Vergangenheit ein paar Mal darüber nachgedacht, es aber nie versucht. Nun zur magischen Frage: Wie gut hat es funktioniert? Nur für spärliche Rätsel oder auch für dichte (wie in der Zeitung)? Und wie viele Hinweise brauchen Sie für dichte Rätsel?
dmckee --- Ex-Moderator Kätzchen
1
@dmckee, es ist eine Weile her, aber aus dem Gedächtnis waren sogar die dichten Rätsel ziemlich gut. Viele wurden ohne Intervention abgeschlossen, aber Sie würden vielleicht noch ein Fünftel bekommen, wenn Sie ein oder zwei zusätzliche Wörter hinzufügen müssten. Und wir sprechen über Tausende von Wörtern in der Datei. Zweifellos hätte das Zurückverfolgen helfen können, aber es war für den Kunden einfacher, eines mit (z. B.) 5 unvollendeten Wörtern abzulehnen, als sich Gedanken darüber zu machen, zusätzliche Hinweise zu finden. Fünf war ungefähr die äußere Grenze, die ich für unvollendete Wörter sah.
Paxdiablo
16

Dieser Algorithmus erstellt in 60 Sekunden 50 dichte Kreuzworträtsel mit 6 x 9 Pfeilen . Es verwendet eine Wortdatenbank (mit Wort + Tipps) und eine Board-Datenbank (mit vorkonfigurierten Boards).

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

Eine größere Wortdatenbank verkürzt die Generierungszeit erheblich und einige Boards sind schwerer zu füllen! Größere Bretter benötigen mehr Zeit, um richtig gefüllt zu werden!


Beispiel:

Vorkonfigurierte 6x9-Karte:

(# bedeutet eine Spitze in einer Zelle,% bedeutet zwei Spitzen in einer Zelle, Pfeile nicht gezeigt)

# - # # - % # - # 
- - - - - - - - - 
# - - - - - # - - 
% - - # - # - - - 
% - - - - - % - - 
- - - - - - - - - 

Generiertes 6x9 Board:

# 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 

Tipps [Zeile, Spalte]:

[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
quelle
11

Obwohl dies eine ältere Frage ist, werde ich versuchen, eine Antwort zu finden, die auf ähnlichen Arbeiten basiert, die ich durchgeführt habe.

Es gibt viele Ansätze zur Lösung von Einschränkungsproblemen (die im Allgemeinen in der NPC-Komplexitätsklasse liegen).

Dies hängt mit der kombinatorischen Optimierung und der Einschränkungsprogrammierung zusammen. In diesem Fall sind die Einschränkungen die Geometrie des Gitters und die Anforderung, dass Wörter eindeutig sind usw.

Randomisierungs- / Annealing-Ansätze können ebenfalls funktionieren (wenn auch innerhalb der richtigen Einstellung).

Effiziente Einfachheit könnte die ultimative Weisheit sein!

Die Anforderungen waren für einen mehr oder weniger vollständigen Kreuzworträtsel-Compiler und einen (visuellen WYSIWYG-) Builder.

Abgesehen vom WYSIWYG-Builder-Teil lautete die Compiler-Gliederung wie folgt:

  1. Laden Sie die verfügbaren Wortlisten (sortiert nach Wortlänge, dh 2,3, .., 20).

  2. Suchen Sie die Wortschlitze (dh Gitterwörter) im vom Benutzer erstellten Gitter (z. B. Wort bei x, y mit der Länge L, horizontal oder vertikal) (Komplexität O (N)).

  3. Berechnen Sie die Schnittpunkte der Gitterwörter (die gefüllt werden müssen) (Komplexität O (N ^ 2))

  4. Berechnen Sie die Schnittpunkte der Wörter in den Wortlisten mit den verschiedenen Buchstaben des verwendeten Alphabets (dies ermöglicht die Suche nach übereinstimmenden Wörtern mithilfe einer Vorlage, z. B. einer von cwc verwendeten Sik-Cambon-These ) (Komplexität O (WL * AL)).

Mit den Schritten .3 und .4 können Sie diese Aufgabe ausführen:

ein. Die Schnittpunkte der Gitterwörter mit sich selbst ermöglichen es, eine "Vorlage" für den Versuch zu erstellen, Übereinstimmungen in der zugehörigen Wortliste der verfügbaren Wörter für dieses Gitterwort zu finden (indem die Buchstaben anderer sich überschneidender Wörter mit diesem Wort verwendet werden, die bereits zu einem bestimmten Zeitpunkt gefüllt sind Schritt des Algorithmus)

b. Die Schnittpunkte der Wörter in einer Wortliste mit dem Alphabet ermöglichen es, passende (Kandidaten-) Wörter zu finden, die einer bestimmten "Vorlage" entsprechen (z. B. "A" an erster Stelle und "B" an dritter Stelle usw.)

Mit diesen implementierten Datenstrukturen wurde folgender Algorithmus verwendet:

HINWEIS: Wenn das Raster und die Wortdatenbank konstant sind, können die vorherigen Schritte nur einmal ausgeführt werden.

  1. Der erste Schritt des Algorithmus besteht darin, einen leeren Wortschlitz (Gitterwort) zufällig auszuwählen und ihn mit einem Kandidatenwort aus der zugehörigen Wortliste zu füllen (die Zufallsgenerierung ermöglicht es, bei aufeinanderfolgenden Ausführungen des Algorithmus unterschiedliche Lösungen zu erzeugen) (Komplexität O (1) oder O () N))

  2. Berechnen Sie für jeden noch leeren Wortschlitz (der Schnittpunkte mit bereits gefüllten Wortschlitzen aufweist) ein Einschränkungsverhältnis (dies kann variieren, etw ist einfach die Anzahl der verfügbaren Lösungen in diesem Schritt) und sortieren Sie die leeren Wortschlitze nach diesem Verhältnis (Komplexität O (NlogN) ) oder O (N))

  3. Durchlaufen Sie die leeren Wortfelder, die im vorherigen Schritt berechnet wurden, und versuchen Sie für jedes einzelne eine Reihe von Stornierungslösungen (stellen Sie sicher, dass die "Lichtbogenkonsistenz erhalten bleibt", dh das Raster hat nach diesem Schritt eine Lösung, wenn dieses Wort verwendet wird) und sortieren Sie sie nach maximale Verfügbarkeit für den nächsten Schritt (dh der nächste Schritt hat eine maximal mögliche Lösung, wenn dieses Wort zu diesem Zeitpunkt an diesem Ort verwendet wird usw.) (Komplexität O (N * MaxCandidatesUsed))

  4. Füllen Sie dieses Wort aus (markieren Sie es als gefüllt und fahren Sie mit Schritt 2 fort).

  5. Wenn kein Wort gefunden wird, das die Kriterien von Schritt .3 erfüllt, versuchen Sie, zu einer anderen Kandidatenlösung eines vorherigen Schritts zurückzukehren (Kriterien können hier variieren) (Komplexität O (N)).

  6. Wenn ein Backtrack gefunden wurde, verwenden Sie die Alternative und setzen Sie optional bereits gefüllte Wörter zurück, die möglicherweise zurückgesetzt werden müssen (markieren Sie sie erneut als nicht gefüllt) (Komplexität O (N)).

  7. Wenn kein Backtrack gefunden wird, kann keine Lösung gefunden werden (zumindest mit dieser Konfiguration, dem anfänglichen Startwert usw.)

  8. Andernfalls haben Sie eine Lösung, wenn alle Wordlots gefüllt sind

Dieser Algorithmus führt einen zufälligen konsistenten Durchlauf des Lösungsbaums des Problems durch. Wenn es irgendwann eine Sackgasse gibt, wird ein vorheriger Knoten zurückverfolgt und eine andere Route verfolgt. Bis entweder eine gefundene Lösung oder die Anzahl der Kandidaten für die verschiedenen Knoten erschöpft ist.

Der Konsistenzteil stellt sicher, dass eine gefundene Lösung tatsächlich eine Lösung ist, und der zufällige Teil ermöglicht es, unterschiedliche Lösungen in unterschiedlichen Ausführungen zu erzeugen und im Durchschnitt auch eine bessere Leistung zu erzielen.

PS. All dies (und andere) wurden in reinem JavaScript (mit paralleler Verarbeitung und WYSIWYG) implementiert

PS2. Der Algorithmus kann leicht parallelisiert werden, um mehr als eine (unterschiedliche) Lösung gleichzeitig zu erzeugen

Hoffe das hilft

Nikos M.
quelle
1
Ist dies zum Erstellen dichter Layouts (wie in der NY Times) oder spärlicher Layouts gedacht?
Jim
1
@ Jim, dies ist meistens für dichte Layouts, kann aber auch für spärliche angepasst werden. Der Unterschied besteht in dichten Layouts (z. B. klassisch, skandinavisch usw.). Man hat das Raster und sucht nach Wörtern, während bei Freiform-Layouts (spärlich) die Wörter und Suchen nach einem Raster vorhanden sind.
Nikos M.
1
Hätten Sie irgendwo eine Beispielquelle zur Verfügung, die die obigen Schritte implementiert? Zum Beispiel bin ich für das meiste bei Ihnen (und habe das meiste bereits unabhängig implementiert), aber wenn es darum geht, "ein Constraint-Verhältnis zu berechnen ...", muss ich zugeben, dass Sie mich verloren haben. Web-Suchen nach Dingen wie "STH Ratio" sind auch für mich keine große Hilfe. Das Problem bei meiner Implementierung besteht darin, dass die Wörter zum Ausfüllen sehr ineffizient sind und viel zu lange dauern.
Jim
1
@ Jim, ich habe, da dies bereits verwendet wird, aber dies wurde speziell für einen Job gemacht, den ich hatte. Es ist möglich, dass ich eine leichte Version in meinen Open Source-Projekten veröffentlichen werde, wenn Sie weitere Hilfe benötigen, kontaktieren Sie mich (ps in der Tat bei In einigen Fällen kann der von mir gepostete Algorithmus zu lange dauern, aber im Durchschnitt nicht)
Nikos M.
1
@ Jim, werfen Sie einen Blick auf diese Kreuzworträtsel-Website (noch in Bearbeitung) istavrolexo.gr (auf Griechisch) mit verschiedenen (dichten) Kreuzworträtseln (z. B. Skandinavisch, Klassisch, Sudoku), die mit einem ähnlichen Algorithmus ( einem großen skandinavischen Kreuzworträtsel ) generiert wurden.
Nikos M.
9

Warum nicht einfach einen zufälligen probabilistischen Ansatz verwenden? Beginnen Sie mit einem Wort und wählen Sie dann wiederholt ein zufälliges Wort aus und versuchen Sie, es in den aktuellen Status des Puzzles zu integrieren, ohne die Einschränkungen für die Größe usw. zu brechen. Wenn Sie versagen, beginnen Sie einfach von vorne.

Sie werden überrascht sein, wie oft ein solcher Monte-Carlo-Ansatz funktioniert.

Yogi
quelle
2
Ja, das ist der Ansatz, den ich gewählt habe. Sie müssen nicht versuchen, sehr klug zu sein. Ordnen Sie Wörter vom längsten zum kürzesten. Wählen Sie in einer Schleife eine zufällige Zelle (Spalten- und Zeilenkoordinate) aus und platzieren Sie das Wort auf der Tafel, um festzustellen, ob es am Ende abläuft oder ein anderes Wort stört (bevor Sie das Wort in das Raster schreiben, überprüfen Sie, ob es sich bei jeder Zelle um eine handelt leer oder wenn es einen Buchstaben hat, stimmt dieser Buchstabe mit dem Buchstaben überein, den Sie schreiben möchten). Es gibt eine andere Logik, um Grenzen und Dinge zu überprüfen. I Brute Force erzeugt eine Reihe immer kleinerer Gitter und ordnet dann die kleinsten anhand sich überschneidender Wörter.
Max Hodges
6

Hier ist ein JavaScript-Code, der auf der Antwort von Nickf und dem Python-Code von Bryan basiert. Posten Sie es einfach für den Fall, dass jemand anderes es in js benötigt.

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
quelle
Wortobjektschema fehlt, bitte geben Sie wordArray
dh
Buchstäblich nur eine Reihe von Wörtern wie ['Apfel', 'Orange', 'Birne']
FascistDonut
Hallo, zu meiner Information, meine Bearbeitung hat nicht viel Code geändert, sondern nur formatiert. Ich weiß, dass es beim Anzeigen inline so chaotisch aussieht, aber wenn Sie die tatsächlichen Änderungen im Code sehen möchten , klicken Sie auf "Side-by-Side-Markdown". Nun ... ich hätte "Formatierten Code" in die Bearbeitungsbeschreibung schreiben sollen, aber meh.
Doppelter Piepton
Wie funktioniert das? Können Sie eine HTML-Datei bereitstellen, die dieses Javascript enthält?
GKS
5

Ich würde zwei Zahlen generieren: Length und Scrabble Score. Angenommen, eine niedrige Scrabble-Punktzahl bedeutet, dass das Beitreten einfacher ist (niedrige Punktzahl = viele gebräuchliche Buchstaben). Sortieren Sie die Liste nach absteigender Länge und aufsteigender Scrabble-Punktzahl.

Als nächstes gehen Sie einfach die Liste durch. Wenn sich das Wort nicht mit einem vorhandenen Wort kreuzt (überprüfen Sie jedes Wort anhand seiner Länge bzw. Scrabble-Punktzahl), stellen Sie es in die Warteschlange und überprüfen Sie das nächste Wort.

Spülen und wiederholen, und dies sollte ein Kreuzworträtsel erzeugen.

Natürlich bin ich mir ziemlich sicher, dass dies O (n!) Ist und es nicht garantiert ist, dass das Kreuzworträtsel für Sie vervollständigt wird, aber vielleicht kann es jemand verbessern.

Eric
quelle
3

Ich habe über dieses Problem nachgedacht. Meiner Meinung nach können Sie nicht hoffen, dass Ihre begrenzte Wortliste ausreicht, um ein wirklich dichtes Kreuzworträtsel zu erstellen. Daher möchten Sie möglicherweise ein Wörterbuch in eine "Trie" -Datenstruktur einfügen. Auf diese Weise können Sie leicht Wörter finden, die die verbleibenden Leerzeichen ausfüllen. In einem Versuch ist es ziemlich effizient, eine Durchquerung zu implementieren, die Ihnen beispielsweise alle Wörter der Form "c? T" gibt.

Mein allgemeiner Gedanke lautet also: Erstellen Sie einen relativ Brute-Force-Ansatz, wie hier beschrieben, um ein Kreuz mit niedriger Dichte zu erstellen, und füllen Sie die Lücken mit Wörterbuchwörtern.

Wenn jemand diesen Ansatz gewählt hat, lassen Sie es mich bitte wissen.

Jake
quelle
3

Ich habe mit der Kreuzworträtsel-Generator-Engine herumgespielt und fand dies am wichtigsten:

0.!/usr/bin/python

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

    b. Erstellen Sie ein Element / Objekt wie einen Cursor, der zur einfachen Orientierung um die Matrix herumgeht, es sei denn, Sie möchten später durch zufällige Auswahl iterieren.

  2. das erste, nimm das erste Paar und platziere es quer und runter von 0,0; Speichern Sie den ersten als unseren aktuellen Kreuzworträtsel-Anführer.

  3. Bewegen Sie den Cursor in diagonaler oder zufälliger Reihenfolge mit größerer diagonaler Wahrscheinlichkeit zur nächsten leeren Zelle

  4. Durchlaufen Sie die Wörter wie und verwenden Sie die Länge des freien Speicherplatzes, um die maximale Wortlänge zu definieren: 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. Um das Wort mit dem freien Speicherplatz zu vergleichen, habe ich Folgendes verwendet:

    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. Ändern Sie nach jedem erfolgreich verwendeten Wort die Richtung. Schleife, während alle Zellen gefüllt sind ODER Ihnen die Wörter ausgehen ODER durch Iterationsbegrenzung, dann:

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

... und wieder neues Kreuzworträtsel wiederholen.

  1. Machen Sie das Bewertungssystem durch einfaches Füllen und einige Schätzungsberechnungen. Geben Sie die Punktzahl für das aktuelle Kreuzworträtsel an und schränken Sie die spätere Auswahl ein, indem Sie es in die Liste der erstellten Kreuzworträtsel einfügen, wenn die Punktzahl von Ihrem Punktesystem erfüllt wird.

  2. Nach der ersten Iterationssitzung iterieren Sie erneut aus der Liste der erstellten Kreuzworträtsel, um den Job zu beenden.

Durch die Verwendung von mehr Parametern kann die Geschwindigkeit um einen großen Faktor verbessert werden.

Alex
quelle
2

Ich würde einen Index jedes Buchstabens erhalten, der von jedem Wort verwendet wird, um mögliche Kreuze zu kennen. Dann würde ich das größte Wort wählen und es als Basis verwenden. Wählen Sie den nächsten großen aus und kreuzen Sie ihn. Spülen und wiederholen. Es ist wahrscheinlich ein NP-Problem.

Eine andere Idee ist die Erstellung eines genetischen Algorithmus, bei dem die Stärke-Metrik angibt, wie viele Wörter Sie in das Raster einfügen können.

Der schwierige Teil, den ich finde, ist, wenn man weiß, dass eine bestimmte Liste unmöglich gekreuzt werden kann.

eipipuz
quelle
1
Ich dachte auch an einen genetischen Algorithmus. Die Fitnessfunktion könnte sein, wie eng die Wörter in das Raster gepackt sind.
Adrian McCarthy
1

jQuery Crossword Puzzle Generator und Spiel

Ich habe eine JavaScript / jQuery-Lösung für dieses Problem codiert:

Beispieldemo: http://www.earthfluent.com/crossword-puzzle-demo.html

Quellcode: https://github.com/HoldOffHunger/jquery-crossword-puzzle-generator

Die Absicht des von mir verwendeten Algorithmus:

  1. Minimieren Sie die Anzahl der unbrauchbaren Quadrate im Raster so weit wie möglich.
  2. Habe so viele gemischte Wörter wie möglich.
  3. Rechnen Sie in extrem schneller Zeit.

Demonstration eines generierten Kreuzworträtsels.

Ich werde den Algorithmus beschreiben, den ich verwendet habe:

  1. Gruppieren Sie die Wörter nach denen, die einen gemeinsamen Buchstaben haben.

  2. Erstellen Sie aus diesen Gruppen Sätze einer neuen Datenstruktur ("Wortblöcke"), bei der es sich um ein primäres Wort (das alle anderen Wörter durchläuft) und dann um die anderen Wörter (die das primäre Wort durchlaufen) handelt.

  3. Beginnen Sie das Kreuzworträtsel mit dem allerersten dieser Wortblöcke ganz oben links im Kreuzworträtsel.

  4. Bewegen Sie sich für den Rest der Wortblöcke ausgehend von der Position ganz rechts unten des Kreuzworträtsels nach oben und links, bis keine freien Plätze mehr verfügbar sind. Wenn mehr leere Spalten nach oben als nach links vorhanden sind, bewegen Sie sich nach oben und umgekehrt.

HoldOffHunger
quelle
@holdoffhunger Haben Sie eine Methode, um den Kreuzworträtselschlüssel anzuzeigen? Boxen mit ausgefüllten Briefen?
Jon Glazer
@ Jon Glazer: Normalerweise senden Sie die Kreuzworträtselschlüssel an die Funktion selbst, aber Sie können das Kreuzworträtsel direkt danach als 2D-Array von Zeichen protokollieren var crosswords = generateCrosswordBlockSources(puzzlewords);. Protokollieren Sie diesen Wert einfach in der Konsole. Vergiss nicht, es gibt einen "Cheat-Modus" im Spiel, in dem du einfach auf "Antwort anzeigen" klicken kannst, um den Wert sofort zu erhalten.
HoldOffHunger
Dies erzeugt Rätsel mit Kauderwelsch "über" Wörter an Stellen mit benachbarten "Ab" -Kästen und umgekehrt. Standard Kreuzworträtsel wie diese nicht funktionieren, wenn es tut , um die Dichte zu maximieren.
Beejor
1

Dieser erscheint als Projekt im AI CS50-Kurs von Harvard. Die Idee ist, das Kreuzworträtsel-Generierungsproblem als ein Problem der Einschränkungszufriedenheit zu formulieren und es durch Zurückverfolgen mit verschiedenen Heuristiken zu lösen, um den Suchraum zu reduzieren.

Zunächst benötigen wir einige Eingabedateien:

  1. Die Struktur des Kreuzworträtsels (das wie das folgende aussieht, z. B. wobei das '#' die nicht zu füllenden Zeichen und das '_' die zu füllenden Zeichen darstellt)

`

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

`

  1. Ein Eingabevokabular (Wortliste / Wörterbuch), aus dem die Kandidatenwörter ausgewählt werden (wie das folgende gezeigt).

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

Nun ist der CSP wie folgt definiert und zu lösen:

  1. Variablen sind so definiert, dass Werte (dh ihre Domänen) aus der Liste der Wörter (Vokabeln) als Eingabe bereitgestellt werden.
  2. Jede Variable wird durch ein 3-Tupel dargestellt: (grid_coordinate, direction, length) wobei die Koordinate den Anfang des entsprechenden Wortes darstellt, die Richtung entweder horizontal oder vertikal sein kann und die Länge als die Länge des Wortes definiert ist, das die Variable sein wird Zugewiesen an.
  3. Die Einschränkungen werden durch die bereitgestellte Struktureingabe definiert: Wenn beispielsweise eine horizontale und eine vertikale Variable einen gemeinsamen Charakter haben, wird dies als Überlappungsbedingung (Bogenbedingung) dargestellt.
  4. Jetzt können die Algorithmen für die Knotenkonsistenz und die AC3-Bogenkonsistenz verwendet werden, um die Domänen zu reduzieren.
  5. Dann kann das Zurückverfolgen, um eine Lösung (falls vorhanden) für den CSP mit MRV (minimaler verbleibender Wert), Grad usw. zu erhalten. Heuristiken können verwendet werden, um die nächste nicht zugewiesene Variable auszuwählen, und Heuristiken wie LCV (niedrigster einschränkender Wert) können für Domänen verwendet werden. Bestellung, um den Suchalgorithmus schneller zu machen.

Das Folgende zeigt die Ausgabe, die unter Verwendung einer Implementierung des CSP-Lösungsalgorithmus erhalten wurde:

`
███S████D█
MUCH████E█
E██A█AGENT
S██R█N██Y█
SUPPLY████
█N███O████
█I██INSIDE
█Q███E██A█
SUGAR███N█
█E██████C█
██OFFENSE█

`

Die folgende Animation zeigt die Schritte zum Zurückverfolgen:

Geben Sie hier die Bildbeschreibung ein

Hier ist eine andere mit einer Wortliste in Bangla (Bengali):

Geben Sie hier die Bildbeschreibung ein

Sandipan Dey
quelle
+1 für eine wirklich interessante Erklärung. Der Kontext für das Problem, das ich hier zu lösen versuchte, war jedoch, dass es eine kleine Menge von Wörtern gab, die alle verwendet werden mussten, und ich versuchte, ein optimales Layout für das Kreuzworträtsel zu finden, anstatt mit einem Layout zu beginnen und Wörter zu finden welche passen.
Nickf