Создать Решатель Судоку на Java

1. Обзор

В этой статье мы рассмотрим головоломку Судоку и алгоритмы, используемые для ее решения.

Далее мы будем внедрять решения на Java. Первым решением будет простая атака грубой силой. Второй будет использовать метод Dancing Links .

Давайте помнить, что основное внимание мы собираемся сосредоточить на алгоритмах, а не на дизайне ООП.

2. Головоломка Судоку

Проще говоря, Sudoku - это комбинаторная головоломка размещения чисел с сеткой ячеек 9 x 9, частично заполненной числами от 1 до 9. Цель состоит в том, чтобы заполнить оставшиеся пустые поля остальными числами, чтобы в каждой строке и столбце был только один количество каждого вида.

Более того, в каждом подразделе 3 × 3 сетки не может быть дублировано также любое число. Уровень сложности естественным образом возрастает с увеличением количества пустых полей на каждой доске.

2.1. Тестовая доска

Чтобы сделать наше решение более интересным и проверить алгоритм, мы будем использовать http://www.telegraph.co.uk/news/science/science-news/9359579/Worlds-hardest-sudoku-can-you -crack-it.html[«Самое сложное судоку в мире»], которое:

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. Решение

Прежде всего, давайте определим нашу доску как двумерный массив целых чисел. Мы будем использовать 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 }
};

Давайте создадим метод solve () , который принимает board в качестве входного параметра и перебирает строки, столбцы и значения, проверяя каждую ячейку на предмет правильного решения:

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;
}

Другой метод, который нам нужен, - это метод isValid () , который будет проверять ограничения Судоку, то есть проверять, являются ли строки, столбцы и сетка 3 x 3 действительными:

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

Эти три проверки относительно похожи. Во-первых, давайте начнем с проверки строк:

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. Танцевальные ссылки

4.1. Точное покрытие

Давайте посмотрим на другое решение. Судоку можно описать как проблему Exact Cover , которая может быть представлена ​​матрицей инцидентности, показывающей отношения между двумя объектами.

Например, если мы берем числа от 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 |

Каждый столбец имеет ровно одну единицу во всех выбранных строках.

4.2. Алгоритм X

Алгоритм X - это «метод проб и ошибок» , чтобы найти все решения точной проблемы покрытия, т. Е. Начиная с нашего примера коллекции S = \ {A, B, C, D, E, F} , найти подколлекцию S ** = \ {B, D, F} .__

Алгоритм X работает следующим образом:

, Если матрица A не имеет столбцов, текущее частичное решение

правильное решение; завершить успешно, в противном случае выберите столбец c (Детерминировано) , Выберите строку r так, чтобы _A ~ r , c_ ~ = 1 (недетерминировано,

то есть попробуй все возможности) , Включить строку r в частичное решение

, Для каждого столбца j такой, что _A ~ r , j ~ = 1, для каждой строки i_

такой, что _A ~ i , j ~ = 1, удалить строку i из матрицы A и удалить столбец j из матрицы A , Повторите этот алгоритм рекурсивно на уменьшенной матрице A_

Эффективной реализацией алгоритма X является алгоритм Dancing Links (сокращенно DLX), предложенный доктором Дональдом Кнутом.

Следующее решение в значительной степени вдохновлено реализацией Java this .

4.3. Точная проблема с обложкой

Во-первых, нам нужно создать матрицу, которая будет представлять головоломку Судоку как проблему точного покрытия. Матрица будет иметь 9 ^ 3 строки, то есть по одной строке на каждую возможную позицию (9 строк x 9 столбцов) каждого возможного числа (9 номеров).

Столбцы будут представлять доску (снова 9 х 9), умноженную на количество ограничений.

Мы уже определили три ограничения:

  • в каждой строке будет только один номер каждого вида

  • в каждом столбце будет только один номер каждого вида

  • каждый подраздел будет иметь только один номер каждого вида

Кроме того, существует неявное четвертое ограничение:

  • в ячейке может быть только один номер

Это дает четыре ограничения в сумме и, следовательно, 9 х 9 х 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;
}

Мы готовы перейти к следующему этапу сейчас. Давайте создадим два класса, которые будут связывать наши клетки вместе.

4.4. Танцующий узел

Алгоритм Dancing Links работает на основе базового наблюдения, что следующая операция над двусвязными списками узлов:

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. Solver

Далее нам нужно создать сетку, состоящую из наших объектов DancingNode и ColumnNode :

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

    ColumnNode headerNode = new ColumnNode("header");
    List<ColumnNode> 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. Ориентиры

Мы можем сравнить эти два разных алгоритма, запустив их на одном компьютере (таким образом мы можем избежать различий в компонентах, скорости процессора или оперативной памяти и т. Д.). Фактическое время будет отличаться от компьютера к компьютеру.

Однако мы должны увидеть относительные результаты, и это скажет нам, какой алгоритм работает быстрее.

Алгоритм обратного отслеживания занимает около 250 мс для решения платы.

Если мы сравним это с Dancing Links, который занимает около 50 мс, мы увидим явного победителя. Танцевальные ссылки примерно в пять раз быстрее при решении этого конкретного примера.

6. Заключение

В этом уроке мы обсудили два решения головоломки судоку с ядром Java. Алгоритм возврата, который является алгоритмом перебора, может легко решить стандартную головоломку 9 × 9.

Немного более сложный алгоритм Dancing Links также обсуждался. Оба решают самые сложные головоломки за считанные секунды.

Наконец, как всегда, код, использованный во время обсуждения, можно найти по адресу https://github.com/eugenp/tutorials/tree/master/algorithms-miscellaneous-2 на GitHub].