回答:
私はおそらく最も効率的ではないソリューションを考え出しましたが、それは十分に機能します。基本的に:
これは機能しますが、かなり貧弱なクロスワードです。より良い結果を得るために、上記の基本的なレシピにいくつかの変更を加えました。
最近、Pythonで独自に作成しました。ここで見つけることができます:http : //bryanhelmig.com/python-crossword-puzzle-generator/。密集したNYTスタイルのクロスワードは作成されませんが、子供のパズルブックにあるクロスワードのスタイルが作成されます。
いくつかの提案されているようなランダムなブルートフォースメソッドを使用して単語を配置するいくつかのアルゴリズムとは異なり、少しスマートなブルートフォースアプローチを単語の配置に実装しようとしました。これが私のプロセスです:
最後に、ほぼ同じなので、適切なクロスワードパズルまたはワード検索パズルができます。うまく動作する傾向がありますが、改善について何か提案があれば教えてください。グリッドが大きくなると、指数関数的に遅くなります。大きな単語は線形にリストされます。単語リストが大きいほど、単語配置数が増える可能性が高くなります。
array.sort(key=f)
は安定しています。つまり、(たとえば)アルファベット順の単語リストを長さで並べ替えると、8文字の単語すべてがアルファベット順に並べ替えられます。
私は実際に約10年前にクロスワード生成プログラムを書きました(それは不可解でしたが、同じルールが通常のクロスワードに適用されます)。
ファイルに格納されている単語(および関連する手がかり)のリストがあり、使用頻度の降順でソートされています(そのため、使用頻度の低い単語がファイルの先頭にあります)。テンプレートは、基本的には黒い正方形と自由な正方形を表すビットマスクであり、クライアントから提供されたプールからランダムに選択されました。
次に、パズルの完全でない単語ごとに(基本的に最初の空白の正方形を見つけて、右側(単語全体)またはその下(単語の下)も空白かどうかを確認します)の検索が行われましたその単語にすでに含まれている文字を考慮して、適合した最初の単語を探すファイル。当てはまる単語がない場合は、単語全体を不完全としてマークして先に進みます。
最後に、コンパイラーが入力する必要があるいくつかの未完成の単語があります(必要に応じて、単語と手掛かりをファイルに追加します)。彼らが思い付くことができなかった場合は任意のアイデアを、彼らは、制約を変更するか、単に総再生成のために頼むために手動でクロスワードを編集することができます。
単語/手がかりファイルが特定のサイズに達すると(このクライアントでは1日あたり50〜100の手がかりが追加されます)、クロスワードごとに2つまたは3つを超える手動修正が必要になることはほとんどありませんでした。 。
このアルゴリズムは、60秒で50個の高密度6x9 矢印クロスワードを作成します。単語データベース(単語とヒントを含む)とボードデータベース(構成済みのボードを含む)を使用します。
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ボード:
(#は1つのセルに1つのチップ、%は1つのセルに2つのチップ、矢印は表示されていない)
# - # # - % # - #
- - - - - - - - -
# - - - - - # - -
% - - # - # - - -
% - - - - - % - -
- - - - - - - - -
生成された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)
ユーザーが作成したグリッド上でワードスロット(つまりグリッドワード)を検索します(たとえば、長さL、水平または垂直のx、yのワード)(複雑度O(N))
グリッドワードの交差点を計算します(塗りつぶす必要があります)(複雑さO(N ^ 2))
使用されているアルファベットのさまざまな文字を使用してワードリスト内の単語の共通部分を計算します(これにより、cwcで使用されているSik Cambon論文などのテンプレートを使用して一致する単語を検索できます)(複雑さO(WL * AL))
ステップ.3および.4では、このタスクを実行できます。
a。グリッドワードとそれ自体との共通部分により、このグリッドワードで使用可能な単語の関連ワードリストで一致を検索するための「テンプレート」を作成できます(特定の単語で既に入力されているこの単語と交差する他の単語の文字を使用)アルゴリズムのステップ)
b。単語リスト内の単語とアルファベットの共通部分により、特定の「テンプレート」に一致する一致(候補)単語を見つけることができます(たとえば、「A」が1位、「B」が3位など)。
したがって、これらのデータ構造を実装すると、使用されるアルゴリズムは次のようになります。
注:グリッドと単語のデータベースが一定の場合、前の手順は一度だけ実行できます。
アルゴリズムの最初のステップは、空のワードスロット(グリッドワード)をランダムに選択し、それに関連付けられたワードリストから候補ワードを入力します(ランダム化により、アルゴリズムの連続実行で異なるソリューションを生成できます)(複雑度O(1)またはO( N))
まだ空の各ワードスロット(既に満たされたワードスロットとの交差がある)について、制約比率を計算します(これは変化する可能性があります。単純には、そのステップで使用可能なソリューションの数です)。この比率で空のワードスロットを並べ替えます(複雑度O(NlogN )またはO(N))
前のステップで計算された空のワードスロットをループし、それぞれについていくつかの候補解を試行し(「アーク一貫性が保持されていること」を確認します。つまり、このワードが使用されている場合、グリッドはこのステップの後に解を持ちます)、次に従ってソートします。次のステップの最大の可用性(つまり、この単語がその場所でそのときに使用されている場合、次のステップには可能な最大の解決策があります)(複雑さO(N * MaxCandidatesUsed))
その単語を入力します(入力済みとしてマークし、ステップ2に進みます)
ステップ.3の基準を満たす単語が見つからない場合は、前のステップの別の候補ソリューションに戻ります(基準はここで異なる場合があります)(複雑さO(N))
バックトラックが見つかった場合は、代わりの方法を使用し、必要に応じて、リセットが必要な可能性のある既に満たされた単語をリセットします(再度、満たされていないものとしてマークします)(複雑さO(N))
バックトラックが見つからない場合、解決策は見つかりません(少なくともこの構成、初期シードなどでは)。
それ以外の場合、すべてのワードロットがいっぱいになると、1つの解決策があります
このアルゴリズムは、問題のソリューションツリーをランダムに一貫してウォークします。ある時点で行き止まりがある場合、前のノードに戻り、別のルートをたどります。解が見つかるか、さまざまなノードの候補の数がなくなるまで。
整合性の部分は、見つかったソリューションが実際にソリューションであることを確認し、ランダムな部分は、異なる実行で異なるソリューションを生成することを可能にし、平均してより良いパフォーマンスを持っています。
PS。これらすべて(およびその他)は、純粋なJavaScript(並列処理とWYSIWYGを使用)機能で実装されました
PS2。アルゴリズムは、複数の(異なる)ソリューションを同時に生成するために簡単に並列化できます。
お役に立てれば
まずは、ランダムな確率論的アプローチを使用しないでください。単語から始めて、ランダムな単語を繰り返し選択し、サイズなどの制約を破ることなく現在のパズルの状態に合わせようとします。失敗した場合は、最初からやり直してください。
このようなモンテカルロアプローチがどれほど頻繁に機能するかは、驚くでしょう。
以下は、nickfの回答とBryanのPythonコードに基づくJavaScriptコードです。誰かが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();
}
私はこの問題について考えてきました。私の感覚では、本当に密なクロスワードを作成するには、限られた単語リストで十分であると期待することはできません。したがって、ディクショナリを取得して「トライ」データ構造に配置することができます。これにより、残りのスペースを埋める単語を簡単に見つけることができます。トライでは、たとえば「c?t」という形式のすべての単語を提供するトラバーサルを実装するのがかなり効率的です。
したがって、私の一般的な考え方は、低密度のクロスを作成するためにここで説明されているようなある種の比較的ブルートフォースアプローチを作成し、空白を辞書の単語で埋めます。
他の誰かがこのアプローチを取っている場合は、私に知らせてください。
私はクロスワードジェネレーターエンジンをいじっていましたが、これが最も重要であることがわかりました。
0。!/usr/bin/python
a。 allwords.sort(key=len, reverse=True)
b。後でランダムに選択してイテレートしたくない場合を除き、簡単な方向付けのために行列を歩き回るカーソルのようなアイテム/オブジェクトを作成します。
最初に、最初のペアをピックアップし、それらを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:][:]
...そして新しいクロスワードを繰り返します。
記入のしやすさや見積もり計算で採点システムを作ります。現在のクロスワードのスコアを指定し、スコアリングシステムによってスコアが満たされている場合は、作成されたクロスワードのリストに追加して、後の選択肢を絞り込みます。
最初の反復セッションの後、作成されたクロスワードのリストから再度反復して、ジョブを終了します。
より多くのパラメーターを使用することにより、速度は大きな要因で改善できます。
可能性のあるクロスを知るために、各単語で使用される各文字のインデックスを取得します。次に、最大の単語を選択し、それをベースとして使用します。次の大きいものを選択し、それを交差させます。すすぎ、繰り返します。おそらくNPの問題です。
別のアイデアは、強度メトリックがグリッドに入れることができる単語数である遺伝的アルゴリズムを作成することです。
私が見つけるのが難しいのは、特定のリストを超えられない可能性があることを知るときです。
この問題のJavaScript / jQueryソリューションをコーディングしました。
サンプルデモ:http : //www.earthfluent.com/crossword-puzzle-demo.html
ソースコード:https : //github.com/HoldOffHunger/jquery-crossword-puzzle-generator
私が使用したアルゴリズムの意図:
私が使用したアルゴリズムについて説明します。
共通の文字を共有するものに従って単語をグループ化します。
これらのグループから、新しいデータ構造(「ワードブロック」)のセットを構築します。これは、(他のすべてのワードを通過する)プライマリワードであり、次に(プライマリワードを通過する)他のワードです。
クロスワードパズルの一番左上にあるこれらのワードブロックの最初のブロックから、クロスワードパズルを開始します。
残りの単語ブロックについては、クロスワードパズルの右下の最も高い位置から始め、埋めるために利用可能なスロットがなくなるまで、左上に移動します。左より上に空の列がある場合は、上に移動し、逆も同様です。
var crosswords = generateCrosswordBlockSources(puzzlewords);
。この値をログに記録するだけです。ゲームには「チートモード」があり、「Reveal Answer」をクリックするだけですぐに値を取得できることを忘れないでください。
これはハーバード大学のAI CS50コースのプロジェクトとして表示されます。アイデアは、クロスワード生成問題を制約充足問題として定式化し、異なるヒューリスティックを使用したバックトラックで解決して、検索スペースを削減することです。
まず、いくつかの入力ファイルが必要です。
`
###_####_#
____####_#
_##_#_____
_##_#_##_#
______####
#_###_####
#_##______
#_###_##_#
_____###_#
#_######_#
##_______#
`
候補となる単語が選択される入力語彙(単語リスト/辞書)(以下に示すような)。
a
abandon
ability
able
abortion
about
above
abroad
absence
absolute
absolutely
...
これで、CSPが定義され、次のように解決されます。
以下は、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█
`
次のアニメーションは、バックトラックの手順を示しています。
バングラ(ベンガル)言語の単語リストを使用した別の例を次に示します。