Javaで数独ソルバーを作成する

Javaで数独ソルバーを作成する

1. 概要

この記事では、数独パズルとそれを解くために使用されるアルゴリズムを見ていきます。

次に、Javaでソリューションを実装します。 最初の解決策は、単純な総当たり攻撃です。 2つ目は、Dancing Links手法を利用します。

ここでは、OOP設計ではなく、アルゴリズムに焦点を当てることに注意してください。

2. 数独パズル

簡単に言えば、数独は、1〜9の数字で部分的に埋められた9 x 9のセルグリッドを備えた組み合わせ数字配置パズルです。 目標は、残りの空白フィールドに残りの数字を入力し、各行と列に各種類の数字が1つだけ含まれるようにすることです。

さらに、グリッドの3 x 3のサブセクションごとに、番号を複製することはできません。 難易度は、各ボードの空白フィールドの数とともに自然に増加します。

2.1. テストボード

ソリューションをより面白くし、アルゴリズムを検証するために、“world’s hardest sudoku”ボードを使用します。これは次のとおりです。

8 . . . . . . . .
. . 3 6 . . . . .
. 7 . . 9 . 2 . .
. 5 . . . 7 . . .
. . . . 4 5 7 . .
. . . 1 . . . 3 .
. . 1 . . . . 6 8
. . 8 5 . . . 1 .
. 9 . . . . 4 . .

2.2. 解決済みボード

また、ソリューションを迅速に台無しにするために、正しく解決されたパズルは次の結果をもたらします。

8 1 2 7 5 3 6 4 9
9 4 3 6 8 2 1 7 5
6 7 5 4 9 1 2 8 3
1 5 4 2 3 7 8 9 6
3 6 9 8 4 5 7 2 1
2 8 7 1 6 9 5 3 4
5 2 1 9 7 4 3 6 8
4 3 8 5 2 6 9 1 7
7 9 6 3 1 8 4 5 2

3. バックトラッキングアルゴリズム

3.1. 前書き

バックトラッキングアルゴリズムは、有効な解決策について各セルをテストすることにより、パズルを解決しようとします。

制約違反がない場合、アルゴリズムは次のセルに移動し、考えられるすべての解決策を入力して、すべてのチェックを繰り返します。

違反がある場合は、セルの値が増加します。 セルの値が9に達すると、まだ違反があり、アルゴリズムは前のセルに戻り、そのセルの値を増やします。

すべての可能な解決策を試みます。

3.2. 溶液

まず、ボードを整数の2次元配列として定義しましょう。 空のセルとして0を使用します。

int[][] board = {
  { 8, 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 3, 6, 0, 0, 0, 0, 0 },
  { 0, 7, 0, 0, 9, 0, 2, 0, 0 },
  { 0, 5, 0, 0, 0, 7, 0, 0, 0 },
  { 0, 0, 0, 0, 4, 5, 7, 0, 0 },
  { 0, 0, 0, 1, 0, 0, 0, 3, 0 },
  { 0, 0, 1, 0, 0, 0, 0, 6, 8 },
  { 0, 0, 8, 5, 0, 0, 0, 1, 0 },
  { 0, 9, 0, 0, 0, 0, 4, 0, 0 }
};

boardを入力パラメーターとして受け取り、行、列、および値を反復処理して各セルの有効なソリューションをテストするsolve()メソッドを作成しましょう。

private boolean solve(int[][] board) {
    for (int row = BOARD_START_INDEX; row < BOARD_SIZE; row++) {
        for (int column = BOARD_START_INDEX; column < BOARD_SIZE; column++) {
            if (board[row][column] == NO_VALUE) {
                for (int k = MIN_VALUE; k <= MAX_VALUE; k++) {
                    board[row][column] = k;
                    if (isValid(board, row, column) && solve(board)) {
                        return true;
                    }
                    board[row][column] = NO_VALUE;
                }
                return false;
            }
        }
    }
    return true;
}

必要なもう1つのメソッドは、isValid()メソッドです。これは、数独制約をチェックします。つまり、行、列、および3 x3グリッドが有効かどうかをチェックします。

private boolean isValid(int[][] board, int row, int column) {
    return (rowConstraint(board, row)
      && columnConstraint(board, column)
      && subsectionConstraint(board, row, column));
}

これら3つのチェックは比較的似ています。 まず、行チェックから始めましょう。

private boolean rowConstraint(int[][] board, int row) {
    boolean[] constraint = new boolean[BOARD_SIZE];
    return IntStream.range(BOARD_START_INDEX, BOARD_SIZE)
      .allMatch(column -> checkConstraint(board, row, constraint, column));
}

次に、ほとんど同じコードを使用して列を検証します。

private boolean columnConstraint(int[][] board, int column) {
    boolean[] constraint = new boolean[BOARD_SIZE];
    return IntStream.range(BOARD_START_INDEX, BOARD_SIZE)
      .allMatch(row -> checkConstraint(board, row, constraint, column));
}

さらに、3 x 3サブセクションを検証する必要があります。

private boolean subsectionConstraint(int[][] board, int row, int column) {
    boolean[] constraint = new boolean[BOARD_SIZE];
    int subsectionRowStart = (row / SUBSECTION_SIZE) * SUBSECTION_SIZE;
    int subsectionRowEnd = subsectionRowStart + SUBSECTION_SIZE;

    int subsectionColumnStart = (column / SUBSECTION_SIZE) * SUBSECTION_SIZE;
    int subsectionColumnEnd = subsectionColumnStart + SUBSECTION_SIZE;

    for (int r = subsectionRowStart; r < subsectionRowEnd; r++) {
        for (int c = subsectionColumnStart; c < subsectionColumnEnd; c++) {
            if (!checkConstraint(board, r, constraint, c)) return false;
        }
    }
    return true;
}

最後に、checkConstraint()メソッドが必要です。

boolean checkConstraint(
  int[][] board,
  int row,
  boolean[] constraint,
  int column) {
    if (board[row][column] != NO_VALUE) {
        if (!constraint[board[row][column] - 1]) {
            constraint[board[row][column] - 1] = true;
        } else {
            return false;
        }
    }
    return true;
}

一度行われると、isValid()メソッドは単にtrueを返すことができます。

これで、ソリューションをテストする準備がほぼ整いました。 アルゴリズムが完了しました。 ただし、trueまたはfalseのみを返します。

したがって、ボードを視覚的に確認するには、結果を印刷するだけです。 どうやら、これはアルゴリズムの一部ではありません。

private void printBoard() {
    for (int row = BOARD_START_INDEX; row < BOARD_SIZE; row++) {
        for (int column = BOARD_START_INDEX; column < BOARD_SIZE; column++) {
            System.out.print(board[row][column] + " ");
        }
        System.out.println();
    }
}

数独パズルを解くバックトラッキングアルゴリズムの実装に成功しました。

明らかに、アルゴリズムは可能な組み合わせを何度も何度も単純にチェックするため、改善の余地があります(特定のソリューションが無効であることがわかっている場合でも)。

4.1. 正確なカバー

別の解決策を見てみましょう。 数独はExact Cover問題として説明できます。これは、2つのオブジェクト間の関係を示す接続行列で表すことができます。

たとえば、1から7までの数と、集合族S = \{A, B, C, D, E, F}をとると、次のようになります。

  • A = \ {1、4、7}

  • B = \ {1、4}

  • C = \ {4、5、7}

  • D = \ {3、5、6}

  • E = \ {2、3、6、7}

  • F = \ {2、7}

私たちの目標は、各番号が一度だけ存在し、何も繰り返されないサブセット、つまり名前を選択することです。

行列を使用して問題を表すことができます。列は数字で、行はセットです。

  | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
A | 1 | 0 | 0 | 1 | 0 | 0 | 1 |
B | 1 | 0 | 0 | 1 | 0 | 0 | 0 |
C | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
D | 0 | 0 | 1 | 0 | 1 | 1 | 0 |
E | 0 | 1 | 1 | 0 | 0 | 1 | 1 |
F | 0 | 1 | 0 | 0 | 0 | 0 | 1 |

サブコレクションS * = \ {B、D、F}は正確なカバーです:

  | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
B | 1 | 0 | 0 | 1 | 0 | 0 | 0 |
D | 0 | 0 | 1 | 0 | 1 | 1 | 0 |
F | 0 | 1 | 0 | 0 | 0 | 0 | 1 |

各列には、選択したすべての行に1が1つだけあります。

4.2. アルゴリズムX

アルゴリズムXは、正確な被覆問題のすべての解を見つけるための“trial-and-error approach”です。 サンプルコレクションS = \{A, B, C, D, E, F}から始めて、サブコレクションS* = \{B, D, F}.を見つけます

アルゴリズムXは次のように機能します。

  1. 行列Aに列がない場合、現在の部分解は有効な解です。正常に終了します。それ以外の場合は、列cを選択します(決定論的に)

  2. Arc〜 = 1となるような行rを選択します(非決定論的に、つまり、すべての可能性を試してください)

  3. 部分解に行rを含めます

  4. Arj〜 = 1となる各列jについて、Aiとなる各行i )s、j〜 = 1、行列Aから行iを削除し、行列Aから_ delete column _jを削除します

  5. 縮小された行列Aでこのアルゴリズムを再帰的に繰り返します

アルゴリズムXの効率的な実装は、博士によって提案されたDancing Linksアルゴリズム(略してDLX)です。 ドナルド・クヌース。

次のソリューションは、thisのJava実装に大きく影響を受けています。

4.3. 正確なカバーの問題

最初に、数独パズルを正確なカバー問題として表すマトリックスを作成する必要があります。 マトリックスには9 ^ 3行、つまり、すべての可能な数(9つの数字)のすべての可能な位置(9行x 9列)ごとに1つの行があります。

列は、制約の数を掛けたボード(再び9 x 9)を表します。

すでに3つの制約を定義しています。

  • 各行には、各種類の番号が1つだけあります

  • 各列には各種類の番号が1つだけあります

  • 各サブセクションには、各種類の番号が1つだけあります

さらに、暗黙的な4番目の制約があります。

  • 1つのセルに含めることができるのは1つの番号のみです

これにより、合計で4つの制約が与えられるため、Exact Coverマトリックスの9 x 9 x 4列になります。

private static int BOARD_SIZE = 9;
private static int SUBSECTION_SIZE = 3;
private static int NO_VALUE = 0;
private static int CONSTRAINTS = 4;
private static int MIN_VALUE = 1;
private static int MAX_VALUE = 9;
private static int COVER_START_INDEX = 1;
private int getIndex(int row, int column, int num) {
    return (row - 1) * BOARD_SIZE * BOARD_SIZE
      + (column - 1) * BOARD_SIZE + (num - 1);
}
private boolean[][] createExactCoverBoard() {
    boolean[][] coverBoard = new boolean
      [BOARD_SIZE * BOARD_SIZE * MAX_VALUE]
      [BOARD_SIZE * BOARD_SIZE * CONSTRAINTS];

    int hBase = 0;
    hBase = checkCellConstraint(coverBoard, hBase);
    hBase = checkRowConstraint(coverBoard, hBase);
    hBase = checkColumnConstraint(coverBoard, hBase);
    checkSubsectionConstraint(coverBoard, hBase);

    return coverBoard;
}

private int checkSubsectionConstraint(boolean[][] coverBoard, int hBase) {
    for (int row = COVER_START_INDEX; row <= BOARD_SIZE; row += SUBSECTION_SIZE) {
        for (int column = COVER_START_INDEX; column <= BOARD_SIZE; column += SUBSECTION_SIZE) {
            for (int n = COVER_START_INDEX; n <= BOARD_SIZE; n++, hBase++) {
                for (int rowDelta = 0; rowDelta < SUBSECTION_SIZE; rowDelta++) {
                    for (int columnDelta = 0; columnDelta < SUBSECTION_SIZE; columnDelta++) {
                        int index = getIndex(row + rowDelta, column + columnDelta, n);
                        coverBoard[index][hBase] = true;
                    }
                }
            }
        }
    }
    return hBase;
}

private int checkColumnConstraint(boolean[][] coverBoard, int hBase) {
    for (int column = COVER_START_INDEX; column <= BOARD_SIZE; c++) {
        for (int n = COVER_START_INDEX; n <= BOARD_SIZE; n++, hBase++) {
            for (int row = COVER_START_INDEX; row <= BOARD_SIZE; row++) {
                int index = getIndex(row, column, n);
                coverBoard[index][hBase] = true;
            }
        }
    }
    return hBase;
}

private int checkRowConstraint(boolean[][] coverBoard, int hBase) {
    for (int row = COVER_START_INDEX; row <= BOARD_SIZE; r++) {
        for (int n = COVER_START_INDEX; n <= BOARD_SIZE; n++, hBase++) {
            for (int column = COVER_START_INDEX; column <= BOARD_SIZE; column++) {
                int index = getIndex(row, column, n);
                coverBoard[index][hBase] = true;
            }
        }
    }
    return hBase;
}

private int checkCellConstraint(boolean[][] coverBoard, int hBase) {
    for (int row = COVER_START_INDEX; row <= BOARD_SIZE; row++) {
        for (int column = COVER_START_INDEX; column <= BOARD_SIZE; column++, hBase++) {
            for (int n = COVER_START_INDEX; n <= BOARD_SIZE; n++) {
                int index = getIndex(row, column, n);
                coverBoard[index][hBase] = true;
            }
        }
    }
    return hBase;
}

次に、新しく作成したボードを最初のパズルレイアウトで更新する必要があります。

private boolean[][] initializeExactCoverBoard(int[][] board) {
    boolean[][] coverBoard = createExactCoverBoard();
    for (int row = COVER_START_INDEX; row <= BOARD_SIZE; row++) {
        for (int column = COVER_START_INDEX; column <= BOARD_SIZE; column++) {
            int n = board[row - 1][column - 1];
            if (n != NO_VALUE) {
                for (int num = MIN_VALUE; num <= MAX_VALUE; num++) {
                    if (num != n) {
                        Arrays.fill(coverBoard[getIndex(row, column, num)], false);
                    }
                }
            }
        }
    }
    return coverBoard;
}

これで、次の段階に進む準備ができました。 セルをリンクする2つのクラスを作成しましょう。

4.4. ダンスノード

ダンシングリンクアルゴリズムは、ノードの二重リンクリストでの次の操作に関する基本的な観察に基づいて動作します。

node.prev.next = node.next
node.next.prev = node.prev

ノードを削除します:

node.prev = node
node.next = node

ノードを復元します。

DLXの各ノードは、上下左右のノードにリンクされています。

DancingNodeクラスには、ノードを追加または削除するために必要なすべての操作が含まれます。

class DancingNode {
    DancingNode L, R, U, D;
    ColumnNode C;

    DancingNode hookDown(DancingNode node) {
        assert (this.C == node.C);
        node.D = this.D;
        node.D.U = node;
        node.U = this;
        this.D = node;
        return node;
    }

    DancingNode hookRight(DancingNode node) {
        node.R = this.R;
        node.R.L = node;
        node.L = this;
        this.R = node;
        return node;
    }

    void unlinkLR() {
        this.L.R = this.R;
        this.R.L = this.L;
    }

    void relinkLR() {
        this.L.R = this.R.L = this;
    }

    void unlinkUD() {
        this.U.D = this.D;
        this.D.U = this.U;
    }

    void relinkUD() {
        this.U.D = this.D.U = this;
    }

    DancingNode() {
        L = R = U = D = this;
    }

    DancingNode(ColumnNode c) {
        this();
        C = c;
    }
}

4.5. 列ノード

ColumnNodeクラスは列をリンクします:

class ColumnNode extends DancingNode {
    int size;
    String name;

    ColumnNode(String n) {
        super();
        size = 0;
        name = n;
        C = this;
    }

    void cover() {
        unlinkLR();
        for (DancingNode i = this.D; i != this; i = i.D) {
            for (DancingNode j = i.R; j != i; j = j.R) {
                j.unlinkUD();
                j.C.size--;
            }
        }
    }

    void uncover() {
        for (DancingNode i = this.U; i != this; i = i.U) {
            for (DancingNode j = i.L; j != i; j = j.L) {
                j.C.size++;
                j.relinkUD();
            }
        }
        relinkLR();
    }
}

4.6. ソルバー

次に、DancingNodeオブジェクトとColumnNodeオブジェクトで構成されるグリッドを作成する必要があります。

private ColumnNode makeDLXBoard(boolean[][] grid) {
    int COLS = grid[0].length;

    ColumnNode headerNode = new ColumnNode("header");
    List columnNodes = new ArrayList<>();

    for (int i = 0; i < COLS; i++) {
        ColumnNode n = new ColumnNode(Integer.toString(i));
        columnNodes.add(n);
        headerNode = (ColumnNode) headerNode.hookRight(n);
    }
    headerNode = headerNode.R.C;

    for (boolean[] aGrid : grid) {
        DancingNode prev = null;
        for (int j = 0; j < COLS; j++) {
            if (aGrid[j]) {
                ColumnNode col = columnNodes.get(j);
                DancingNode newNode = new DancingNode(col);
                if (prev == null) prev = newNode;
                col.U.hookDown(newNode);
                prev = prev.hookRight(newNode);
                col.size++;
            }
        }
    }

    headerNode.size = COLS;

    return headerNode;
}

ヒューリスティック検索を使用して列を検索し、マトリックスのサブセットを返します。

private ColumnNode selectColumnNodeHeuristic() {
    int min = Integer.MAX_VALUE;
    ColumnNode ret = null;
    for (
      ColumnNode c = (ColumnNode) header.R;
      c != header;
      c = (ColumnNode) c.R) {
        if (c.size < min) {
            min = c.size;
            ret = c;
        }
    }
    return ret;
}

最後に、答えを再帰的に検索できます。

private void search(int k) {
    if (header.R == header) {
        handleSolution(answer);
    } else {
        ColumnNode c = selectColumnNodeHeuristic();
        c.cover();

        for (DancingNode r = c.D; r != c; r = r.D) {
            answer.add(r);

            for (DancingNode j = r.R; j != r; j = j.R) {
                j.C.cover();
            }

            search(k + 1);

            r = answer.remove(answer.size() - 1);
            c = r.C;

            for (DancingNode j = r.L; j != r; j = j.L) {
                j.C.uncover();
            }
        }
        c.uncover();
    }
}

列がもうない場合は、解決した数独ボードを印刷できます。

5. ベンチマーク

これら2つの異なるアルゴリズムを同じコンピューターで実行することで比較できます(これにより、コンポーネント、CPUまたはRAMの速度などの違いを回避できます)。 実際の時間はコンピューターによって異なります。

ただし、相対的な結果を確認できるはずです。これにより、どのアルゴリズムがより高速に実行されるかがわかります。

バックトラッキングアルゴリズムは、ボードの解決に約250msかかります。

これを約50ミリ秒かかるDancing Linksと比較すると、明確な勝者がわかります。 この特定の例を解くと、Dancing Linksは約5倍高速になります。

6. 結論

このチュートリアルでは、コアJavaを使用した数独パズルの2つの解決策について説明しました。 ブルートフォースアルゴリズムであるバックトラッキングアルゴリズムは、標準の9×9パズルを簡単に解決できます。

少し複雑なDancing Linksアルゴリズムについても説明しました。 どちらも数秒で最も難しいパズルを解きます。

最後に、いつものように、議論中に使用されたコードはover on GitHubで見つけることができます。