昨天和孩子一起做数独游戏,竟然连入门级的第一道题都做不出来。一怒之下,写了一个小程序,帮助我们做一些简单的分析和判断。
数独(Sudoku)的解题技巧
当某一行(row)、某一列(column)、某一宫(box)中已经出现了8个数字,则余下的一格(cell)一定是第9个数字。
例1:
2 | 5 | 8 | ? | 1 | 4 | 7 | 9 | 6 |
---|
则 ?
处一定是 3
。
例2:
1 | 2 | 3 | ? | 4 | 8 | |||
---|---|---|---|---|---|---|---|---|
6 | ||||||||
5 | ||||||||
7 |
则 ?
处一定是 9
。
例3:
1 | ||||||||
---|---|---|---|---|---|---|---|---|
1 | ||||||||
? | ||||||||
1 | ||||||||
1 | ||||||||
则 ?
处一定是 1
。
下面就是我做的入门级数独第一题,有兴趣的同学可以试试看:
0 0 0 0 0 1 5 4 0
2 1 0 4 0 0 0 0 0
0 4 0 0 3 0 0 0 0
4 0 0 0 1 0 0 3 2
0 0 8 0 6 0 7 0 0
7 9 0 0 4 0 0 0 6
0 0 0 0 8 0 0 9 0
0 0 0 0 0 2 0 8 1
0 8 5 6 0 0 0 0 0
数独有很多解题技巧,我写的程序只包含了最基本的技巧。
程序源代码以及运行效果
完整的代码如下:
package com.company;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Sudoku {
// output
private List<Integer> grid = new ArrayList<Integer>();
// possible values for each cell
private List<List<Integer>> possibles = new ArrayList<List<Integer>>();
// just for print different color on changed cells
private List<Boolean> changed = new ArrayList<Boolean>();
// index list in the same row/column/box
// e.g.: for index "5", the index list in the same row is "0" to "8"
private List<List<Integer>> indexesInRow = new ArrayList<List<Integer>>();
private List<List<Integer>> indexesInColumn = new ArrayList<List<Integer>>();
private List<List<Integer>> indexesInBox = new ArrayList<List<Integer>>();
// print result for each iteration
private boolean info_level = false;
// print result for any update
private boolean debug_level = false;
// finished or not
public boolean isDone() {
return !grid.stream().anyMatch(Objects::isNull);
}
// initialize index row list
private List<Integer> getIndexesInRow(int index) {
int startingIndex = index / 9 * 9;
List<Integer> result = Arrays.asList(startingIndex, startingIndex+1, startingIndex+2, startingIndex+3,
startingIndex+4, startingIndex+5, startingIndex+6, startingIndex+7, startingIndex+8);
return result;
}
// initialize index column list
private List<Integer> getIndexesInColumn(int index) {
int startingIndex = index % 9;
List<Integer> result = Arrays.asList(startingIndex, startingIndex+9, startingIndex+2*9, startingIndex+3*9,
startingIndex+4*9, startingIndex+5*9, startingIndex+6*9, startingIndex+7*9, startingIndex+8*9);
return result;
}
// initialize index box list
private List<Integer> getIndexesInBox(int index) {
int row = index / 9 / 3;
int column = index / 3 * 3 % 9;
int startingIndex = row * 9 * 3 + column;
List<Integer> result = Arrays.asList(startingIndex, startingIndex+1, startingIndex+2, startingIndex+9,
startingIndex+10, startingIndex+11, startingIndex+18, startingIndex+19, startingIndex+20);
return result;
}
private void updatePossibleFromRow(int index) {
List<Integer> possible = possibles.get(index);
indexesInRow.get(index).forEach(e -> {if (e != index && possibles.get(e).size() == 1) possible.removeAll(possibles.get(e));});
for (int i = 1; i <= 9; i++) {
int j = i;
if (indexesInRow.get(index).stream().anyMatch(e -> possibles.get(e).size() == 1 && possibles.get(e).get(0) == j))
continue;
long count = indexesInRow.get(index).stream().filter(e -> e != index && possibles.get(e).contains(j)).count();
if (count == 0) {
possible.clear();
possible.add(i);
break;
}
}
}
private void updatePossibleFromColumn(int index) {
List<Integer> possible = possibles.get(index);
indexesInColumn.get(index).forEach(e -> {if (e != index && possibles.get(e).size() == 1) possible.removeAll(possibles.get(e));});
for (int i = 1; i <= 9; i++) {
int j = i;
if (indexesInColumn.get(index).stream().anyMatch(e -> possibles.get(e).size() == 1 && possibles.get(e).get(0) == j))
continue;
long count = indexesInColumn.get(index).stream().filter(e -> e != index && possibles.get(e).contains(j)).count();
if (count == 0) {
possible.clear();
possible.add(i);
break;
}
}
}
private void updatePossibleFromBox(int index) {
List<Integer> possible = possibles.get(index);
indexesInBox.get(index).forEach(e -> {if (e != index && possibles.get(e).size() == 1) possible.removeAll(possibles.get(e));});
for (int i = 1; i <= 9; i++) {
int j = i;
if (indexesInBox.get(index).stream().anyMatch(e -> possibles.get(e).size() == 1 && possibles.get(e).get(0) == j))
continue;
long count = indexesInBox.get(index).stream().filter(e -> e != index && possibles.get(e).contains(j)).count();
if (count == 0) {
possible.clear();
possible.add(i);
break;
}
}
}
private void updatePossible(int index) {
updatePossibleFromRow(index);
if (debug_level) {
System.out.println("after updatePossibleFromRow " + index);
printPossibles();
}
updatePossibleFromColumn(index);
if (debug_level) {
System.out.println("after updatePossibleFromColumn " + index);
printPossibles();
}
updatePossibleFromBox(index);
if (debug_level) {
System.out.println("after updatePossibleFromBox " + index);
printPossibles();
}
}
private void updateGrid() {
for (int i = 0; i <= grid.size() - 1; i++) {
if (grid.get(i) == null && possibles.get(i).size() == 1) {
changed.set(i, true);
grid.set(i, possibles.get(i).get(0));
}
}
}
public void printPossibles() {
System.out.println("=============== possibles ===============");
for (int i = 0; i <= possibles.size() -1; i++) {
if (i > 0 && i % 3 == 0)
System.out.print("\t");
if (i > 0 && i % 9 == 0)
System.out.println();
if (i > 0 && i % 27 == 0)
System.out.println();
System.out.print(String.format("%-27s", possibles.get(i)) + "\t");
}
System.out.println();
System.out.println();
}
public void print() {
System.out.println("=============== grid ===============");
for (int i = 0; i <= grid.size() -1; i++) {
if (i > 0 && i % 3 == 0)
System.out.print("\t");
if (i > 0 && i % 9 == 0)
System.out.println();
if (i > 0 && i % 27 == 0)
System.out.println();
int x = grid.get(i) == null ? 0 : grid.get(i);
String str = changed.get(i) ? "\033[31;4m" + x + "\033[0m" : "" + x;
System.out.print(str + "\t");
}
System.out.println();
System.out.println();
}
public boolean execute() {
for (int i = 1; i <= 100; i++) {
if (info_level)
System.out.println("=============== iteration " + i + " ===============");
for (int index = 0; index <= 81-1; index++) {
updatePossible(index);
}
updateGrid();
if (info_level) {
printPossibles();
print();
}
if (isDone())
return true;
}
return false;
}
public void setRunLevel(String level) {
if ("info_level".equalsIgnoreCase(level)) {
info_level = true;
debug_level = false;
} else if ("debug_level".equalsIgnoreCase(level)) {
info_level = true;
debug_level = true;
}
}
public Sudoku(int[] gridArray) {
for (int i = 0; i < gridArray.length; i++) {
int e = gridArray[i];
grid.add(e == 0 ? null : e);
possibles.add(e == 0 ? Stream.of(1,2,3,4,5,6,7,8,9).collect(Collectors.toList())
: Stream.of(e).collect(Collectors.toList()));
changed.add(false);
indexesInRow.add(getIndexesInRow(i));
indexesInColumn.add(getIndexesInColumn(i));
indexesInBox.add(getIndexesInBox(i));
}
}
public static void main(String[] args) {
String run_level = null;
if (args.length > 0)
run_level = args[0];
int[] gridArray =
{
0,0,0,0,0,1,5,4,0,
2,1,0,4,0,0,0,0,0,
0,4,0,0,3,0,0,0,0,
4,0,0,0,1,0,0,3,2,
0,0,8,0,6,0,7,0,0,
7,9,0,0,4,0,0,0,6,
0,0,0,0,8,0,0,9,0,
0,0,0,0,0,2,0,8,1,
0,8,5,6,0,0,0,0,0
};
Sudoku sudoku = new Sudoku(gridArray);
sudoku.setRunLevel(run_level);
sudoku.print();
if (sudoku.execute()){
System.out.println("success!");
sudoku.print();
} else {
System.out.println("failed...");
sudoku.print();
sudoku.printPossibles();
}
}
}
运行效果如下:
=============== grid ===============
0 0 0 0 0 1 5 4 0
2 1 0 4 0 0 0 0 0
0 4 0 0 3 0 0 0 0
4 0 0 0 1 0 0 3 2
0 0 8 0 6 0 7 0 0
7 9 0 0 4 0 0 0 6
0 0 0 0 8 0 0 9 0
0 0 0 0 0 2 0 8 1
0 8 5 6 0 0 0 0 0
success!
=============== grid ===============
8 6 3 9 2 1 5 4 7
2 1 9 4 7 5 3 6 8
5 4 7 8 3 6 1 2 9
4 5 6 7 1 8 9 3 2
3 2 8 5 6 9 7 1 4
7 9 1 2 4 3 8 5 6
6 3 2 1 8 7 4 9 5
9 7 4 3 5 2 6 8 1
1 8 5 6 9 4 2 7 3
Process finished with exit code 0
程序说明
实际上,该程序的解题思路非常简单:排除掉所有不可能的值,剩下的一个值即为确定的值。
具体算法如下:
创建一个可能值(possible)矩阵,把每个cell的每个可能值都枚举出来。初始化时,根据cell是否有值,分为两种情况:
- 无值:则possible列表包含从1到9的所有值,即
[1, 2, 3, 4, 5, 6, 7, 8, 9]
- 有值:则possible列表只包含该确定值,例如
[7]
Possible初始矩阵如下:
[1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1] [5] [4] [1, 2, 3, 4, 5, 6, 7, 8, 9]
[2] [1] [1, 2, 3, 4, 5, 6, 7, 8, 9] [4] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 5, 6, 7, 8, 9] [4] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [3] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9]
[4] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [3] [2]
[1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [8] [1, 2, 3, 4, 5, 6, 7, 8, 9] [6] [1, 2, 3, 4, 5, 6, 7, 8, 9] [7] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9]
[7] [9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [4] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [6]
[1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [8] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [9] [1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [2] [1, 2, 3, 4, 5, 6, 7, 8, 9] [8] [1]
[1, 2, 3, 4, 5, 6, 7, 8, 9] [8] [5] [6] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9]
遍历每个cell:
-
在同一row里查看其它cell,如果有已经确定的值,则从本cell的可能值列表中remove该值。
例如:第一个cell,初始化时,其可能值列表为[1, 2, 3, 4, 5, 6, 7, 8, 9]
,从同一row中remove其它cell的确定值之后,变成[2, 3, 6, 7, 8, 9]
。 -
遍历数字1到9,以
1
为例,如果同一row里的所有其它cell都不可能为1
,则可确定本cell值为1
。 -
把row换成column,重复上述操作。
-
把row换成box,重复上述操作。
-
遍历下一个cell。如果所有81个cell都遍历完了,则查看是否完成(所有cell都有确定值)。如果还未完成,则开启下一轮遍历。
例如:cell (4, 3),其初始化可能值列表为 [1, 2, 3, 4, 5, 6, 7, 8, 9]
。在第一轮遍历中,通过同一row,排除了 [1, 2, 3, 4]
,通过同一column,排除了 [5, 8]
,通过同一box,排除了 [7, 9]
,所以最终可以确定其值为 6
。
为了方便看到每一步更新的过程,程序里设置了2个参数:
- info_level:每次迭代完成后,打印出数独和possibles矩阵
- debug_level:possibles矩阵有任何更新,都打印出来
以info_level为例,第一次迭代完成后,打印如下:
0 0 0 0 0 1 5 4 0
2 1 0 4 0 0 0 0 0
0 4 0 0 3 0 0 0 0
4 0 6(new) 0 1 0 0 3 2
0 0 8 0 6 0 7 0 4(new)
7 9 0 0 4 0 0 0 6
0 0 0 0 8 0 0 9 0
0 0 0 0 0 2 0 8 1
0 8 5 6 0 0 0 0 0
标为 (new)
的 6
和 4
是第一次迭代后确定的值。实际上在运行程序时,会以红色显示有变化(即确定)的值。如下图:
如果还不太清楚 6
和 4
是如何被确定的,可以使用debug_level来打印出计算过程,此处不再赘述。
我测试了几个数独题目,都很快解出来了,但是由于程序只用到了最基本的技巧,有些需要高级技巧才能解决的数独题目,估计还是做不出来的。为了防止死循环,我设置了最大迭代数量为100,超过100就认输了。当然,如果改为判断“如果当前迭代和上次迭代没有差别则认输”会更好一些。
由于时间仓促,以上就是数独小程序的思路和代码的小结,以后有机会再慢慢完善。
附
代码位置: https://github.com/dukeding/sudoku