通过回溯解决数独 (java)

Solve sudoku by backtracking (java)

public static int[][] solve(int[][] input){

        for (int i = 0; i < 9*9; i++){
            if(input[i / 9][i % 9] != 0){
                continue;
            }
            for (int j = 1; j <= 9; j++){
                    if(validNumber(input, i / 9, i % 9, j)){
                        input[i / 9][i % 9] = j;
                        solve(input);
                    }
            }
        }
        return input;
    }

无论初始情况如何,此方法都应通过回溯解决(可解决的)数独难题。它是这样工作的:

给定一个数独游戏,它从每行的左上角迭代到二维数组的右下角。当已经有一个数字时,它会被跳过。当存在零(空字段)时,它通过 validNumber 方法计算可能的值。第一个有效数字(从 1 到 9)被放入该字段,该方法转到下一个字段。

在此算法中,该方法现在不考虑有效数字是否最终会导致拼图无法解决。

我想这样修改它:

最后,当该方法完成对整个二维数组的迭代时,将测试数组的每个条目是否为零。

即使有一个零,整个算法也必须转到第一个 "valid" 数字被放入的地方。现在,下一个 "valid" 数字被放入,依此类推,直到算法末尾没有零。

我在实现这个想法时遇到了一些麻烦。在我看来,某处必须有另一个 for 循环,或者类似 goto 语句的东西,但我不知道该放在哪里。

有什么建议吗?

我以前实现过一次数独解算器。它比您拥有的要复杂一些,但眨眼间就解决了游戏。 :)

您正在尝试做的是通过 "Brute Force" 并使用(尾)递归来解决数独问题。这意味着您正在尝试通过遍历所有 981 可能的组合来解决棋盘问题。 9 的 81 次方是……这是一个很大的数字。因此,您的方法将花费永恒,但您将 运行 从尾递归中 space 出栈。

当我实现数独时,它更直接了。它保留了一个 "items" 的 9x9 数组,其中每个项目都是正方形中的值,以及一个代表候选人的 9 个布尔值数组(true == viable,false == eliminated)。然后它只是做了一个非递归循环来解决这个问题。

主循环将从寻找仅剩 1 个候选方格的简单过程开始。然后下一步将根据已经分配的值进行简单的候选消除。然后它将进入更复杂的消除技术,例如 X-Wing.

最终 validNumber() 方法不会 return 任何数字,因为没有任何可能性,这意味着之前的选择之一是不正确的。试想算法是从空格子开始的(显然这个谜题是可以解的1)。

解决方案是保留可能的选择树,如果某些选择不正确,则只需将它们从树中删除并使用下一个可用的选择(或者退回到树的更高级别,如果没有选择留在这个分支)。如果有的话,这个方法应该找到解决方案。 (实际上这就是我前段时间实现我的数独解算器的方式。)


1 恕我直言,有 3 种不同的数独:

  1. "true"正确的数独有一个唯一的完整解决方案;

  2. 具有多个不同完整解决方案的模棱两可的数独,例如一个只有 7 个不同数字的谜题,因此它至少有两个不同的解决方案,通过交换第 8 个和第 9 个数字来区分;

  3. 没有完整解法的错误数独,例如同一行出现两次或多次相同的数字。

根据这个定义,求解器算法应该:

  1. 证明无解;

  2. return满足初始网格的完整解

在 "true" 数独的情况下,根据定义,结果是 "true" 解。在模棱两可的数独的情况下,结果可能因算法而异。空网格是模棱两可的数独游戏的终极例子。

你的算法实际上并没有回溯。如果可以,它会向前移动,但当它意识到自己被困在角落时,它永远不会向后移动。这是因为它从不 returns 堆栈中的任何知识,也从不重置方块。除非你真的很幸运,否则你的代码会让游戏板进入角落状态,然后打印出那个角落状态。要回溯,您需要将您设置的最后一个方块(让您走投无路的那个)重置为零,这样您的算法就会知道继续尝试其他事情。

为了理解回溯,我强烈推荐 Steven Skiena 的一本名为《算法设计手册》的书。我在准备 SWE 面试时读了它,它确实提高了我对回溯、复杂性和图搜索的知识。本书后半部分是75道classic算法题的目录,数独就是其中之一!他对您可以进行的优化进行了有趣的分析,以修剪搜索树并解决非常困难的拼图板。下面是我很久以前读完本章后写的一些代码(按照我目前的标准可能不是那么高质量,但它可以工作)。我很快地通读了它并在 solve 方法中添加了 solveSmart 布尔值,它允许您打开或关闭这些优化之一,这在解决 "hard" class 数独板(一开始只有 17 个方格填满)。

public class Sudoku {

  static class RowCol {
    int row;
    int col;

    RowCol(int r, int c) {
      row = r;
      col = c;
    }
  }

  static int numSquaresFilled;
  static int[][] board = new int[9][9];

  static void printBoard() {
    for (int i = 0; i < 9; i++) {
      for (int j = 0; j < 9; j++) {
        System.out.print(" " + (board[i][j] == 0 ? " " : board[i][j]) + " ");
        if (j % 3 == 2 && j < 8)
          System.out.print("|");
      }
      System.out.println();
      if (i % 3 == 2 && i < 8)
        System.out.println("---------|---------|---------");
    }
    System.out.println();
  }

  static boolean isEntireBoardValid() {
    for (int i = 0; i < 9; i++) {
      for (int j = 0; j < 9; j++) {
        if (!isBoardValid(i, j)) {
          return false;
        }
      }
    }
    return true;
  }

  static boolean isRowValid(int row) {
    int[] count = new int[9];
    for (int col = 0; col < 9; col++) {
      int n = board[row][col] - 1;
      if (n == -1)
        continue;
      count[n]++;
      if (count[n] > 1)
        return false;
    }
    return true;
  }

  static boolean isColValid(int col) {
    int[] count = new int[9];
    for (int row = 0; row < 9; row++) {
      int n = board[row][col] - 1;
      if (n == -1)
        continue;
      count[n]++;
      if (count[n] > 1)
        return false;
    }
    return true;
  }

  static boolean isSquareValid(int row, int col) {
    int r = (row / 3) * 3;
    int c = (col / 3) * 3;
    int[] count = new int[9];
    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 3; j++) {
        int n = board[r + i][c + j] - 1;
        if (n == -1)
          continue;
        count[n]++;
        if (count[n] > 1)
          return false;
      }
    }
    return true;
  }

  static boolean isBoardValid(int row, int col) {
    return (isRowValid(row) && isColValid(col) && isSquareValid(row, col));
  }

  static RowCol getOpenSpaceFirstFound() {
    for (int i = 0; i < 9; i++) {
      for (int j = 0; j < 9; j++) {
        if (board[i][j] == 0) {
          return new RowCol(i, j);
        }
      }
    }
    return new RowCol(0, 0);
  }

  static RowCol getOpenSpaceMostConstrained() {
    int r = 0, c = 0, max = 0;
    int[] rowCounts = new int[9];
    int[] colCounts = new int[9];
    for (int i = 0; i < 9; i++) {
      for (int j = 0; j < 9; j++) {
        if (board[i][j] != 0)
          rowCounts[i]++;
        if (board[j][i] != 0)
          colCounts[i]++;
      }
    }

    int[][] squareCounts = new int[3][3];
    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 3; j++) {
        int count = 0;
        for (int m = 0; m < 3; m++) {
          for (int n = 0; n < 3; n++) {
            if (board[(i * 3) + m][(j * 3) + n] != 0)
              count++;
          }
        }
        squareCounts[i][j] = count;
      }
    }

    for (int i = 0; i < 9; i++) {
      for (int j = 0; j < 9; j++) {
        if (board[i][j] == 0) {
          if (rowCounts[i] > max) {
            max = rowCounts[i];
            r = i;
            c = j;
          }
          if (colCounts[j] > max) {
            max = rowCounts[j];
            r = i;
            c = j;
          }
        }
      }
    }
    return new RowCol(r, c);
  }

  static boolean solve() {
    if (81 == numSquaresFilled) {
      return true;
    }

    boolean solveSmart = true;
    RowCol rc = solveSmart ? getOpenSpaceMostConstrained() : getOpenSpaceFirstFound();
    int r = rc.row;
    int c = rc.col;
    for (int i = 1; i <= 9; i++) {
      numSquaresFilled++;
      board[r][c] = i;
      if (isBoardValid(r, c)) {
        if (solve()) {
          return true;
        }
      }
      board[r][c] = 0;
      numSquaresFilled--;
    }
    return false;
  }

  public static void main(String[] args) {

    // initialize board to a HARD puzzle
    board[0][7] = 1;
    board[0][8] = 2;
    board[1][4] = 3;
    board[1][5] = 5;
    board[2][3] = 6;
    board[2][7] = 7;
    board[3][0] = 7;
    board[3][6] = 3;
    board[4][3] = 4;
    board[4][6] = 8;
    board[5][0] = 1;
    board[6][3] = 1;
    board[6][4] = 2;
    board[7][1] = 8;
    board[7][7] = 4;
    board[8][1] = 5;
    board[8][6] = 6;
    numSquaresFilled = 17;

    printBoard();
    long start = System.currentTimeMillis();
    solve();
    long end = System.currentTimeMillis();
    System.out.println("Solving took " + (end - start) + "ms.\n");
    printBoard();
  }
}