数独问题有一个很重要的概念是“相关20格”,就是指一个单元格它的同行、同列以及同一个小九宫格的共计20个小单元格
。每个单元格的变化只会直接影响它的相关20格。
首先为每个单元格建立一个候选列表,并且初始化的时候用已经给出的提示数对每个单元格的候选列表进行排除。然后循环对每个单元格进行试数,每进行一次试数,就需要更新这个单元格的相关20格的候选列表,如果这个单元格的相关20格中某个单元格的候选列表只有一个数,那么把这个单元格设置成确定状态,同时再对这个单元格的相关20格的候选列表进行排除(这是一个递归的过程)。重复这个过程,直至所有的单元格都已经是固定状态则结束程序。
注意:循环和递归的过程中最好每次更改状态的时候都新建一个状态,也就是使用Immutable模式,这样可以显著降低程序的复杂性,降低出错的可能。
import java.util.*;
public class Sudoku {
//数独的行数和列数
private static final int ROW_LIMIT = 9;
private static final int COL_LIMIT = 9;
private static final int TOTAL_NUM = ROW_LIMIT * COL_LIMIT;
private static final int PLACEHOLDER = 0;
//小九宫格的大小和数目
private static final int ROW_BLK_LIMIT = 3; //每行三个小九宫格
private static final int COL_BLK_LIMIT = 3; //每行三个小九宫格
private static final int ROW_BLK_NUM = 3; //每行小九宫格包含三个小格子
private static final int COL_BLK_NUM = 3; //每列小包含三个小格子
static class State {
final Cell[][] cells = new Cell[ROW_LIMIT][COL_LIMIT];
int fixedNum = 0; //已经确定下来的小方格数
public State() {
}
//(row, col)是将要被固定的小方格的位置,num是它将要被填入的数字
public State(Cell[][] cellsNeedCopy, int fixedNum, int row, int col, int num) {
this.fixedNum = fixedNum;
for (int i = 0; i < ROW_LIMIT; i++) {
for (int j = 0; j < COL_LIMIT; j++) {
if (i == row && j == col) {
cells[i][j] = new Cell(num, true, null);
continue;
}
cells[i][j] = new Cell(cellsNeedCopy[i][j]);
}
}
}
public void init(int[][] data) {
//计算每一行中出现的数字
Map<Integer, Set<Integer>> rowSet = new HashMap<>();
//计算每一列中出现的数字
Map<Integer, Set<Integer>> colSet = new HashMap<>();
//计算第一个小九宫格中出现的数字
Map<Integer, Set<Integer>> cellSet = new HashMap<>();
for (int i = 0; i < 9; i++) {
rowSet.put(i, new HashSet<>());
colSet.put(i, new HashSet<>());
cellSet.put(i, new HashSet<>());
}
//先初始化State的Cell矩阵
for (int i = 0; i < ROW_LIMIT; i++) {
for (int j = 0; j < COL_LIMIT; j++) {
cells[i][j] = new Cell();
cells[i][j].num = data[i][j];
if (data[i][j] != 0) {
rowSet.get(i).add(data[i][j]);
colSet.get(j).add(data[i][j]);
cellSet.get((i / COL_BLK_NUM) * ROW_BLK_LIMIT + j / COL_BLK_NUM).add(data[i][j]);
fixedNum++;
cells[i][j].fixed = true;
cells[i][j].candidates = null;
} else {
cells[i][j].fixed = false;
cells[i][j].candidates = new HashSet<>();
for (int k = 1; k <= 9; k++) {
cells[i][j].candidates.add(k);
}
}
}
}
//尽可能地减少候选列表中的数字
for (int i = 0; i < ROW_LIMIT; i++) {
for (int j = 0; j < COL_LIMIT; j++) {
if (cells[i][j].fixed) continue;
cells[i][j].candidates.removeAll(rowSet.get(i));
cells[i][j].candidates.removeAll(colSet.get(j));
cells[i][j].candidates.removeAll(cellSet.get((i / COL_BLK_NUM) * ROW_BLK_LIMIT + j / COL_BLK_NUM));
}
}
}
}
static class Cell {
int num;
boolean fixed;
Set<Integer> candidates = new HashSet<>();
public Cell(int num, boolean fixed, Set<Integer> candidates) {
this.num = num;
this.fixed = fixed;
this.candidates = new HashSet<>();
if (candidates != null) {
for (int i : candidates) {
this.candidates.add(i);
}
}
}
public Cell() {
}
public Cell(final Cell c) {
num = c.num;
fixed = c.fixed;
candidates = new HashSet<>();
if (c.candidates != null) {
for (int i : c.candidates) {
this.candidates.add(i);
}
}
}
}
public void printState(final State state) {
for (int i = 0; i < ROW_LIMIT; i++) {
for (int j = 0; j < COL_LIMIT; j++) {
System.out.print(state.cells[i][j].num + " ");
}
System.out.println();
}
}
//寻找第(row, col)个小方格的答案
public void findSolution(final State state, int row, int col) {
// System.out.println("Now (" + row + ", " + col + ")");
if (state.fixedNum == TOTAL_NUM) {
System.out.println("Find solution in findSolution!");
printState(state);
return;
}
if (row >= ROW_LIMIT || col >= COL_LIMIT) return;
Cell current = state.cells[row][col];
if (current.fixed) {
int index = row * ROW_LIMIT + col + 1; //用于获得下一个小方格的行和列
findSolution(state, index / ROW_LIMIT, index % ROW_LIMIT);
} else {
//对于这个小方格,遍历尝试其所有的候选列表
for (int num : current.candidates) {
State newState = setCellToFixed(state, row, col, num);
if (newState != null) { //如果把index设置成num是合理的,并且已经从其相关20格的候选列表中去除了num
if (newState.fixedNum == TOTAL_NUM) {
return;
}
int index = row * ROW_LIMIT + col + 1; //用于获得下一个小方格的行和列
findSolution(newState, index / ROW_LIMIT, index % ROW_LIMIT);
}
}
}
}
private State setCellToFixed(final State state, int row, int col, int num) {
final State newState = new State(state.cells, state.fixedNum + 1, row, col, num);
if (newState.fixedNum == TOTAL_NUM) {
System.out.println("Find solution!");
printState(newState);
return newState;
}
//检查其相关20格是否合理
if (!isExclusive(newState, row, col, num)) return null;
//检查其相关20格是否有某个小方格的候选列表是唯一的,如果有,直接设置成那个数
final State newnewState = processSingleCandidate(newState, row, col);
if (newnewState == null) return null;
return newnewState;
}
//此函数可以在原有的State上面操作
private State processSingleCandidate(State state, int row, int col) {
Cell cell;
//遍历行
for (int c = 0; c < ROW_LIMIT; c++) {
cell = state.cells[row][c];
if (!cell.fixed && cell.candidates.size() == 1) {
int num = cell.candidates.iterator().next();
State newState = setCellToFixed(state, row, c, num);
if (newState == null) return null;
state = newState;
if (state.fixedNum == TOTAL_NUM) {
return state;
}
}
}
//遍历列
for (int r = 0; r < COL_LIMIT; r++) {
cell = state.cells[r][col];
if (!cell.fixed && cell.candidates.size() == 1) {
int num = cell.candidates.iterator().next();
State newState = setCellToFixed(state, r, col, num);
if (newState == null) return null;
state = newState;
if (state.fixedNum == TOTAL_NUM) {
return state;
}
}
}
//遍历其所在的小九宫格
int baseRow = (row / ROW_BLK_NUM) * ROW_BLK_NUM;
int baseCol = (col / COL_BLK_NUM) * COL_BLK_NUM;
for (int r = 0; r < ROW_BLK_NUM; r++) {
for (int c = 0; c < COL_BLK_NUM; c++) {
cell = state.cells[baseRow + r][baseCol + c];
if (!cell.fixed && cell.candidates.size() == 1) {
int num = cell.candidates.iterator().next();
// System.out.println("Find sole solution to cell: (" + r + " ," + c + ") = " + num);
State newState = setCellToFixed(state, baseRow + r, baseCol + c, num);
if (newState == null) return null;
state = newState;
if (state.fixedNum == TOTAL_NUM) {
return state;
}
}
}
}
return state;
}
//检查将第row行第col列的小方格设置成num是否合理(通过检查其相关20格是否有重复的num),并在检查的过程中从其相关20格的候选列表中去除num
private boolean isExclusive(final State state, int row, int col, int num) {
return rowExclusive(state, row, col, num) && colExclusive(state, row, col, num) && blockExclusive(state, row, col, num);
}
//确定num是否在当前状态下小九宫格内唯一,并从相应小方格的候选列表中去除num
private boolean blockExclusive(final State state, int row, int col, int num) {
int baseRow = (row / ROW_BLK_NUM) * ROW_BLK_NUM;
int baseCol = (col / COL_BLK_NUM) * COL_BLK_NUM;
Cell cell;
for (int r = 0; r < ROW_BLK_NUM; r++) {
for (int c = 0; c < COL_BLK_NUM; c++) {
if (baseRow + r == row && baseCol + c == col) continue;
cell = state.cells[baseRow + r][baseCol + c];
if (cell.num == num) return false;
if (!cell.fixed) {
cell.candidates.remove(num);
if (cell.candidates.isEmpty()) return false;
}
}
}
return true;
}
//确定num是否在当前状态下列内唯一,并从相应小方格的候选列表中去除num
private boolean colExclusive(final State state, int row, int col, int num) {
Cell cell;
for (int r = 0; r < COL_LIMIT; r++) {
if (r == row) continue;
cell = state.cells[r][col];
if (cell.num == num) return false;
if (!cell.fixed) {
cell.candidates.remove(num);
if (cell.candidates.isEmpty()) return false;
}
}
return true;
}
//确定num是否在当前状态下行内唯一,并从相应小方格的候选列表中去除num
private boolean rowExclusive(final State state, int row, int col, int num) {
Cell cell;
for (int i = 0; i < ROW_LIMIT; i++) {
if (i == col) continue;
cell = state.cells[row][i];
if (cell.num == num) return false;
if (!cell.fixed) {
cell.candidates.remove(num);
if (cell.candidates.isEmpty()) return false;
}
}
return true;
}
public static void main(String[] args) {
// int[][] data = {
// {0, 0, 0, 0, 0, 6, 0, 0, 1},
// {9, 0, 0, 0, 0, 0, 3, 7, 6},
// {7, 1, 0, 0, 4, 0, 0, 0, 0},
// {1, 7, 0, 8, 0, 0, 0, 0, 3},
// {0, 3, 0, 0, 0, 0, 0, 1, 0},
// {6, 0, 0, 0, 0, 3, 0, 5, 8},
// {0, 0, 0, 0, 3, 0, 0, 6, 5},
// {3, 5, 1, 0, 0, 0, 0, 0, 2},
// {8, 0, 0, 1, 0, 0, 0, 0, 0}
// };
int[][] data = {
{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}
};
State state = new State();
state.init(data);
new Sudoku().findSolution(state, 0, 0);
}
}