算法背景
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。
输入:board = [[“5”,“3”,“.”,“.”,“7”,“.”,“.”,“.”,“.”],[“6”,“.”,“.”,“1”,“9”,“5”,“.”,“.”,“.”],[“.”,“9”,“8”,“.”,“.”,“.”,“.”,“6”,“.”],[“8”,“.”,“.”,“.”,“6”,“.”,“.”,“.”,“3”],[“4”,“.”,“.”,“8”,“.”,“3”,“.”,“.”,“1”],[“7”,“.”,“.”,“.”,“2”,“.”,“.”,“.”,“6”],[“.”,“6”,“.”,“.”,“.”,“.”,“2”,“8”,“.”],[“.”,“.”,“.”,“4”,“1”,“9”,“.”,“.”,“5”],[“.”,“.”,“.”,“.”,“8”,“.”,“.”,“7”,“9”]]
输出:[[“5”,“3”,“4”,“6”,“7”,“8”,“9”,“1”,“2”],[“6”,“7”,“2”,“1”,“9”,“5”,“3”,“4”,“8”],[“1”,“9”,“8”,“3”,“4”,“2”,“5”,“6”,“7”],[“8”,“5”,“9”,“7”,“6”,“1”,“4”,“2”,“3”],[“4”,“2”,“6”,“8”,“5”,“3”,“7”,“9”,“1”],[“7”,“1”,“3”,“9”,“2”,“4”,“8”,“5”,“6”],[“9”,“6”,“1”,“5”,“3”,“7”,“2”,“8”,“4”],[“2”,“8”,“7”,“4”,“1”,“9”,“6”,“3”,“5”],[“3”,“4”,“5”,“2”,“8”,“6”,“1”,“7”,“9”]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
独立思考
第一步:本人没玩过数独,所以先去用网上的方法,手动解了一下这道数独,这里不展开说了,放个解题草稿。主要的方法还是题目上说的三条规则,然后就开始构思这么编写代码
第二步:马上想到暴力破解,当遇到没有填值的格子,可以通过那三条规则,先把不能填的值都集中起来,然后挨个遍历能用的值。比如坐标为(0,2)这个空格,先可以通过规则排除掉(3.5.6.7.8.9),所以挨个遍历,先填1,然后往下一个空格走,如果有值就跳过,没有值就按这个方法继续往下走。当出现通过那三条规则把所以的可能值都排除了,但是还没填完。就说明这个解不能用,这个时候我们就应该回溯上去,重新开始遍历,当每一种可能都遍历完,坐标到(8,8)的时候,就找到了唯一解。总结下来有几个关键点,一个是需要用到递归和回溯算法,一个是找个零界点返回错误案例,描述有点绕,可以往下看代码备注
代码示例
public static boolean success = false;
public static void main(String[] args) {
String[][] arrs = {{"5","3",".",".","7",".",".",".","."},{"6",".",".","1","9","5",".",".","."},{".","9","8",".",".",".",".","6","."},{"8",".",".",".","6",".",".",".","3"},{"4",".",".","8",".","3",".",".","1"},{"7",".",".",".","2",".",".",".","6"},{".","6",".",".",".",".","2","8","."},{".",".",".","4","1","9",".",".","5"},{".",".",".",".","8",".",".","7","9"}};
String[][] oldArrs = {{"5","3",".",".","7",".",".",".","."},{"6",".",".","1","9","5",".",".","."},{".","9","8",".",".",".",".","6","."},{"8",".",".",".","6",".",".",".","3"},{"4",".",".","8",".","3",".",".","1"},{"7",".",".",".","2",".",".",".","6"},{".","6",".",".",".",".","2","8","."},{".",".",".","4","1","9",".",".","5"},{".",".",".",".","8",".",".","7","9"}};
sudoku(arrs, oldArrs,0,0);
System.out.println(Arrays.deepToString(arrs));
}
public static void sudoku(String[][] arrs, String[][] oldArrs, int i, int j){
// 成功的条件
if (i == 8 && j == 8){
success = true;
return;
}
// 当空格本身有值就直接往下走
if (!arrs[i][j].equals(".")){
if (j == 8){
sudoku(arrs, oldArrs, ++i, 0);
} else{
sudoku(arrs, oldArrs, i, ++j);
}
return;
}
// 按那三条规则找出排除的值
Set<String> excludeValues = excludeValue(arrs, i, j);
// 临界点
if (excludeValues.size() == 10){
return;
}
// 开始挨个遍历每一种可能性
for (int k = 1;k < 10;k++){
if (!excludeValues.contains(Integer.toString(k))){
arrs[i][j] = Integer.toString(k);
if (j == 8){
sudoku(arrs, oldArrs, i + 1, 0);
} else{
sudoku(arrs, oldArrs, i, j + 1);
}
if (success){
return;
}
// 回溯,还原回去,这步很关键
if (oldArrs[i][j].equals(".")){
arrs[i][j] = ".";
}
}
}
}
public static Set<String> excludeValue(String[][] arrs, int i, int j){
Set<String> excludeSet = new HashSet<>();
for (int i1 = 0; i1 < arrs.length;i1++) {
for (int j1 = 0; j1 < arrs[0].length; j1++) {
// 行
if (i1 == i) {
excludeSet.add(arrs[i1][j1]);
}
// 列
if (j1 == j) {
excludeSet.add(arrs[i1][j1]);
}
// 九宫格
if (i < 3){
v(3, j, i1, j1, arrs, excludeSet);
} else if (i < 6){
v(6, j, i1, j1, arrs, excludeSet);
} else if (i < 9){
v(9, j, i1, j1, arrs, excludeSet);
}
}
}
return excludeSet;
}
public static void v(int i, int j, int i1, int j1, String[][] arrs, Set<String> excludeSet){
if (j < 3){
if (i1 > i - 3 && i1 < i && j1 < 3){
excludeSet.add(arrs[i1][j1]);
}
} else if (j < 6){
if (i1 > i - 3 && i1 < i && j1 > 3 && j1 < 6){
excludeSet.add(arrs[i1][j1]);
}
} else if (j < 9){
if (i1 > i - 3 && i1 < i&& j1 > 6 && j1 < 9){
excludeSet.add(arrs[i1][j1]);
}
}
}