web-dev-qa-db-ja.com

クロスワードパズルを解く

クロスワードパズルとそれを解くために使用できる単語のリストがあります(単語は複数回配置できますまたは1回でも配置できません)。与えられたクロスワードと単語リストには常に解決策があります。

この問題を解決する方法の手がかりを探したところ、NP完全であることがわかりました。私の最大クロスワードサイズは250x 250で、リストの最大長(それを解決するために使用できる単語の量)は200です。私の目標は、ブルートフォース/バックトラッキングによってこのサイズのクロスワードを解決することです。数秒(これは私による大まかな見積もりです。間違っている場合は訂正してください)。

例:

クロスワードを解くために使用できる与えられた単語のリスト:

  • できる
  • 音楽
  • ツナ
  • こんにちは

指定された空のクロスワード(Xは入力できないフィールドであり、空のフィールドは入力する必要があります):

An empty crossword which needs to be solved

ソリューション:

The solution of the problem above

現在の私の現在のアプローチは、クロスワードを2次元配列として表現し、空のスペースを検索することです(クロスワードを2回繰り返します)。次に、単語の長さに応じて単語を空のスペースに一致させ、次に単語のすべての組み合わせを同じ長さの空のスペースに一致させます。このアプローチは非常に速く非常に厄介になりました、私はこれを実装しようとして迷子になりました、よりエレガントな解決策はありますか?

10
Anna Vopureta

あなたが持っている基本的な考え方はかなり賢明です:

  1. ボード上のスロットを特定します。
  2. 適合する各単語で各スロットを試してください。
  3. すべてのスロットを競合することなく埋めることができれば、それは解決されます。

それは素晴らしい計画です。次のステップは、それをデザインに変換することです。このような小さなプログラムの場合、疑似コードに直接進むことができます。その要点は、他の回答で説明されているように、 再帰 :です。

_1  Draw a slot from the slot pool.
2     If slot pool is empty (all slots filled), stop solving.
3  For each Word with correct length:
4     If part of the slot is filled, check conflict.
5        If the Word does not fit, continue the loop to next Word.
      // No conflict
6     Fill the slot with the Word.
      // Try next slot (down a level)
7     Recur from step 1.
8     If the recur found no solution, revert (take the Word back) and try next.
   // None of them works
9  If no words yield a solution, an upper level need to try another Word.
   Revert (put the slot back) and go back.
_

以下は、私があなたの要件から作り上げた短いが完全な例です。

猫の皮を剥ぐ方法は複数あります。私のコードはステップ1と2を交換し、ステップ4と6を1つのフィルループに結合します。

キーポイント:

  • フォーマッターを使用して、コードを自分のスタイルに合わせます。
  • 2Dボードは、線形文字配列に 行優先順 で格納されます。
  • これにより、ボードをclone()で保存し、 arraycopy で復元できます。
  • 作成時に、ボードは2つの方向から2つのパスでスロットをスキャンされます。
  • 2つのスロットリストは同じループで解決されますが、主にスロットの充填方法が異なります。
  • 繰り返しプロセスが表示されるので、それがどのように機能するかを確認できます。
  • 多くの仮定がなされています。一文字のスロットがない、すべての単語が同じ場合、ボードが正しいなど。
  • 我慢して。新しいことは何でも学び、それを吸収する時間を自分に与えてください。

ソース:

_import Java.awt.Point;
import Java.util.*;
import Java.util.function.BiFunction;
import Java.util.function.Supplier;
import Java.util.stream.Stream;

public class Crossword {

   public static void main ( String[] args ) {
      new Crossword( Arrays.asList( "5 4 4\n#_#_#\n_____\n#_##_\n#_##_\ntuna\nmusic\ncan\nhi".split( "\n" ) ) );
      new Crossword( Arrays.asList( "6 6 4\n##_###\n#____#\n___#__\n#_##_#\n#____#\n##_###\nnice\npain\npal\nid".split( "\n" ) ) );
   }

   private final int height, width; // Board size
   private final char[] board; // Current board state.  _ is unfilled.  # is blocked.  other characters are filled.
   private final Set<String> words; // List of words
   private final Map<Point, Integer> vertical = new HashMap<>(), horizontal = new HashMap<>();  // Vertical and horizontal slots

   private String indent = ""; // For formatting log
   private void log ( String message, Object... args ) { System.out.println( indent + String.format( message, args ) ); }

   private Crossword ( List<String> lines ) {
      // Parse input data
      final int[] sizes = Stream.of( lines.get(0).split( "\\s+" ) ).mapToInt( Integer::parseInt ).toArray();
      width = sizes[0];  height = sizes[1];
      board = String.join( "", lines.subList( 1, height+1 ) ).toCharArray();
      words = new HashSet<>( lines.subList( height+1, lines.size() ) );
      // Find horizontal slots then vertical slots
      for ( int y = 0, size ; y < height ; y++ )
         for ( int x = 0 ; x < width-1 ; x++ )
            if ( isSpace( x, y ) && isSpace( x+1, y ) ) {
               for ( size = 2 ; x+size < width && isSpace( x+size, y ) ; size++ ); // Find slot size
               horizontal.put( new Point( x, y ), size );
               x += size; // Skip past this horizontal slot
            }
      for ( int x = 0, size ; x < width ; x++ )
         for ( int y = 0 ; y < height-1 ; y++ )
            if ( isSpace( x, y ) && isSpace( x, y+1 ) ) {
               for ( size = 2 ; y+size < height && isSpace( x, y+size ) ; size++ ); // Find slot size
               vertical.put( new Point( x, y ), size );
               y += size; // Skip past this vertical slot
            }
      log( "A " + width + "x" + height + " board, " + vertical.size() + " vertical, " + horizontal.size() + " horizontal." );
      // Solve the crossword, horizontal first then vertical
      final boolean solved = solveHorizontal();
      // Show board, either fully filled or totally empty.
      for ( int i = 0 ; i < board.length ; i++ ) {
         if ( i % width == 0 ) System.out.println();
         System.out.print( board[i] );
      }
      System.out.println( solved ? "\n" : "\nNo solution found\n" );
   }

   // Helper functions to check or set board cell
   private char get ( int x, int y ) { return board[ y * width + x ]; }
   private void set ( int x, int y, char character ) { board[ y * width + x ] = character; }
   private boolean isSpace ( int x, int y ) { return get( x, y ) == '_'; }

   // Fit all horizontal slots, when success move to solve vertical.
   private boolean solveHorizontal () {
      return solve( horizontal, this::fitHorizontal, "horizontally", this::solveVertical );
   }
   // Fit all vertical slots, report success when done
   private boolean solveVertical () {
      return solve( vertical, this::fitVertical, "vertically", () -> true );
   }

   // Recur each slot, try every Word in a loop.  When all slots of this kind are filled successfully, run next stage.
   private boolean solve ( Map<Point, Integer> slot, BiFunction<Point, String, Boolean> fill, String dir, Supplier<Boolean> next ) {
      if ( slot.isEmpty() ) return next.get(); // If finished, move to next stage.
      final Point pos = slot.keySet().iterator().next();
      final int size = slot.remove( pos );
      final char[] state = board.clone();
      /* Try each Word */                                                   indent += "  ";
      for ( String Word : words ) {
         if ( Word.length() != size ) continue;
         /* If the Word fit, recur. If recur success, done! */              log( "Trying %s %s at %d,%d", Word, dir, pos.x, pos.y );
         if ( fill.apply( pos, Word ) && solve( slot, fill, dir, next ) )
            return true;
         /* Doesn't match. Restore board and try next Word */               log( "%s failed %s at %d,%d", Word, dir, pos.x, pos.y );
         System.arraycopy( state, 0, board, 0, board.length );
      }
      /* No match.  Restore slot and report failure */                      indent = indent.substring( 0, indent.length() - 2 );
      slot.put( pos, size );
      return false;
   }

   // Try fit a Word to a slot.  Return false if there is a conflict.
   private boolean fitHorizontal ( Point pos, String Word ) {
      final int x = pos.x, y = pos.y;
      for ( int i = 0 ; i < Word.length() ; i++ ) {
         if ( ! isSpace( x+i, y ) && get( x+i, y ) != Word.charAt( i ) ) return false; // Conflict
         set( x+i, y, Word.charAt( i ) );
      }
      return true;
   }
   private boolean fitVertical ( Point pos, String Word ) {
      final int x = pos.x, y = pos.y;
      for ( int i = 0 ; i < Word.length() ; i++ ) {
         if ( ! isSpace( x, y+i ) && get( x, y+i ) != Word.charAt( i ) ) return false; // Conflict
         set( x, y+i, Word.charAt( i ) );
      }
      return true;
   }
}
_

演習:次のことができます 書き換え 反復への再帰。より速く、より大きなボードをサポートできます。それが完了すると、マルチスレッドに変換してさらに高速に実行できます。

4
Sheepy

クロスワードパズルは、一般にNP完全である制約充足問題ですが、指定した制約問題に最も効率的なアルゴリズムを適用するソルバーはたくさんあります。 Z3 SMTソルバーは、これらの問題を非常に簡単かつ大規模に解決できます。あなたがしなければならないのは、クロスワードパズルをソルバーが理解できるSMT問題に変換するJavaプログラムを作成し、それをソルバーに渡して解決することです。Z3はJavaバインディングなので、かなり単純なはずです。以下の最初の例を解くためのZ3コードを作成しました。Javaプログラムのパターンに従うのは難しいことではありません。任意に大きなクロスロードパズルを指定します。

; Declare each possible Word as string literals
(define-const str1 String "tuna")
(define-const str2 String "music")
(define-const str3 String "can")
(define-const str4 String "hi")

; Define a function that returns true if the given String is equal to one of the possible words defined above.
(define-fun validString ((s String)) Bool 
    (or (= s str1) (or (= s str2) (or (= s str3) (= s str4)))))

; Declare the strings that need to be solved
(declare-const unknownStr1 String)
(declare-const unknownStr2 String)
(declare-const unknownStr3 String)
(declare-const unknownStr4 String)

; Assert the correct lengths for each of the unknown strings.
(assert (= (str.len unknownStr1) 4))
(assert (= (str.len unknownStr2) 5))
(assert (= (str.len unknownStr3) 3))
(assert (= (str.len unknownStr4) 2))

; Assert each of the unknown strings is one of the possible words.
(assert (validString unknownStr1))
(assert (validString unknownStr2))
(assert (validString unknownStr3))
(assert (validString unknownStr4))

; Where one Word in the crossword puzzle intersects another assert that the characters at the intersection point are equal.
(assert (= (str.at unknownStr1 1) (str.at unknownStr2 1)))
(assert (= (str.at unknownStr2 3) (str.at unknownStr4 1)))
(assert (= (str.at unknownStr2 4) (str.at unknownStr3 0)))

; Solve the model
(check-sat)
(get-model)

Z3 SMTソルバーをお勧めしますが、他にもたくさんの制約ソルバーがあります。独自のソートアルゴリズムを実装する必要がある以上に、独自の制約解決アルゴリズムを実装する必要はありません。

1
Adam

この問題をより簡単に解決できるように、これをより小さく、より簡単な問題に分解します。ここでは役に立たないと思うので、コード/アルゴリズムは含めないことに注意してください(最高のコードが必要な場合は、インデックスとデータベース、そしてそれを見るだけで頭が爆発する黒魔術があります)。代わりに、この回答は、読者に最適な方法を使用して、OPがこの問題(および将来の問題)に取り組むのに役立つ思考方法について話すことによって、質問に答えようとします。

あなたが知る必要があること

この回答は、次の方法を知っていることを前提としています

  • プロパティと機能を持つオブジェクトを作成して使用する
  • その内容で何をしたいのか(必ずしも良いとは限らない)のデータ構造を選択してください。

空間のモデリング

したがって、クロスワードをn行m列の行列(2D配列、ここでは「グリッド」)にロードするのは簡単ですが、これは実用的に機能すると非常によく耳にします。それでは、クロスワードをグリッドから正当なオブジェクトに解析することから始めましょう。

プログラムが知る必要がある限り、クロスワードの各エントリには4つのプロパティがあります。

  1. 最初の文字のグリッド内のX-Y座標
  2. 方向(下または横)
  3. ワード長
  4. 単語の価値
  5. バインドされたインデックスのマップ
    • キー:別のエントリと共有されている単語のインデックス
    • 値:インデックスが共有されるエントリ
    • (これをタプルにして、他のエントリの共有インデックスを含めて簡単に参照できます)

スキャン中にこれらのルールに基づいてグリッドでこれらを見つけることができます。

  1. Row_1_upが閉じていて、Row_1_downが開いている場合、これはダウンしたWordの開始インデックスです。 (長さをスキャンダウンします。バインドされたインデックスの場合、左または右のスペースが開きます。左にスキャンして、リンクされたエントリcoord-idを取得します)
  2. 1と同じですが、単語間でローテーションされます(1のスキャンと同時にこれを行うことができます)

クロスワードオブジェクトでは、座標と方向をキーとして使用してエントリを保存し、テキストグリッドフォームとの間で簡単に参照および変換できます。

モデルの使用

これで、関連するインデックスバインディングを含むクロスワードエントリのコレクションを含むオブジェクトができました。ここで、すべてのエントリを満たす値のセットを見つける必要があります。

エントリオブジェクトには、指定された値とクロスワードの現在の状態をチェックするisValidEntry(str)のようなヘルパーメソッドが必要です。このWordをここに配置できますか?モデル内の各オブジェクトが独自のレベルのロジックを担当するようにすることで、1つの思考層の上の問題のコードは、実装を気にせずにロジックを呼び出すことができます(この例では、ソルバーはロジックを気にする必要はありません)。 ofは有効な値であり、isValidEntryにそのことを要求できます)

上記の権利を実行した場合、問題を解決するには、すべてのエントリのすべての単語を繰り返し処理して解決策を見つけるだけです。

サブ問題のリスト

参考までに、解決するために何かを書く必要があるサブ問題のリストを次に示します。

  • 作業しやすいワークスペースを理想的にモデル化するにはどうすればよいですか?
  • モデルの各部分について、何を知る必要がありますか?それは私のためにどのようなロジックを処理できますか?
  • テキスト入力を使用可能なモデルオブジェクトに変換するにはどうすればよいですか?
  • モデルオブジェクトを使用して問題を解決するにはどうすればよいですか? (あなたにとって、それは有効なセットを見つけるためにすべての単語/すべてのエントリを繰り返すことです。多分再帰を使用します)
1
Tezra

問題はNP-完全です。したがって、最善のチャンスはブルートフォースでそれを解決することです(多項式アルゴリズムを見つけた場合は教えてください、私たちは両方とも金持ちになることができます=))。

私があなたに提案するのは、バックトラッキングを見てみることです。これにより、クロスワード問題に対するエレガントな(ただし入力サイズを考えると遅い)ソリューションを作成できます。

より刺激的な資料が必要な場合は、ソリューションツリーをナビゲートする方法としてバックトラッキングを使用する このソルバー を参照してください。

実際には、純粋なブルートフォースよりも優れたパフォーマンスを発揮する可能性のあるアルゴリズムが存在することに注意してください(まだ指数関数的に複雑ですが)。また、scholarをすばやく検索すると、次のように、調べたいトピックに関する多数の論文が見つかります。

1
Davide Spataro