版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
leetcode 37题,自动解数独
老婆听说我在研究自动解数独,赞叹地说这是不是人工智能啊。咳咳,脸红中,其实没那么玄乎,就是一道算法题,只不过其题材是大家喜闻乐见的数独而已。
2013年时,那时还在工行,刚海外调回来,工作上比较空,且那时候有个大新闻,一个中国农民解出了“史上最难数独”,我也跃跃欲试。
于是鼓捣了一个工作日,用C写了一个算法出来,自测通过并发表到内部技术论坛上。为啥不用熟悉的Java?那时工作机性能不行,打开一个当前要维护的银行Java项目已经有不流畅的感觉,我实在不想再开一个Java IDE进程,用记事本写Java又不太习惯。后来是用C在UltraEdit里面写好并用gcc编译的。
leetcode 37题
也是数独题目
解出来后是如下:
解题思路
数独的解题思路有两种:
一种是正向递推的“消元法”
模拟人脑解数独的方法,就是统计每个单元格横向,竖向,宫格这三个维度中其他数字,并将本单元格[1-9]这9种可能性减去其他数字,剩下如果只有一个数字,那就是本单元格确定的数字,这种方法不需要回溯,但是只能应对简单级别的数独题目。
这种方法下的测试案例
[["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"]]
另一种是反向解题的“递归法”
从map取一个待定的点,遍历其set范围,当某值时,设置board,删map,然后递归下一步,发现不行要回退。注意这里的关键是“回退”,就是上一个单元格尝试性地定个值(可能后来发现不合适,这个值会被推翻),这个单元格在此基础上继续尝试,如果哪一步发现尝试不下去了,就回退掉该单元格取值。
这种方法下的测试案例
[[".",".","9","7","4","8",".",".","."],["7",".",".",".",".",".",".",".","."],[".","2",".","1",".","9",".",".","."],[".",".","7",".",".",".","2","4","."],[".","6","4",".","1",".","5","9","."],[".","9","8",".",".",".","3",".","."],[".",".",".","8",".","3",".","2","."],[".",".",".",".",".",".",".",".","6"],[".",".",".","2","7","5","9",".","."]]
消元法解决数独问题的Java代码
package com.zzz.life;
import java.util.*;
public class Sudo {
class Point {
int i; //二维数组下标
int j; //二维数组下标
int id; //所在方格id,比如11区
Point(int i, int j) {
this.i = i;
this.j = j;
this.id = (i<3?1:(i<6?2:3))*10 + (j<3?1:(j<6?2:3));
}
@Override
public boolean equals(Object obj) {
return obj instanceof Point && (this.i==((Point)obj).i && this.j==((Point)obj).j);
}
@Override
public int hashCode() {
return (i+j+id)%1024;
}
}
private static final int SIZE = 9;
private Set<Character> cloneSet() {
final char[] possibility = new char[]{'1','2','3','4','5','6','7','8','9'};
Set<Character> ret = new HashSet<>();
for (char i : possibility) {
ret.add(i);
}
return ret;
}
private void setInit(char[][] board, Map<Point, Set<Character>> map, Queue<Point> q) {
map.clear();
q.clear();
for (int i=0; i<SIZE; i++) {
for (int j=0; j<SIZE; j++) {
Point tmp = new Point(i,j);
if (board[i][j] == '.') {
map.put(tmp, cloneSet());
} else {
q.offer(tmp);
}
}
}
System.out.println(map.size() + "|" + q.size());
}
private void demention(char[][] board, Map<Point, Set<Character>> map, Queue<Point> q) {
Point tmp = q.poll();
if (map.containsKey(tmp)) map.remove(tmp);
char posval = board[tmp.i][tmp.j];
for (Point point : map.keySet()) {
if (point.i == tmp.i || point.j == tmp.j || point.id == tmp.id) {
Set<Character> posset = map.get(point);
posset.remove(posval);
if (posset.size() == 1) {
q.offer(point);
char c = posset.iterator().next();
board[point.i][point.j] = c;
}
}
}
}
private boolean check(char[][] board, Point point, char val) {
for (int i=0; i<SIZE; i++) {
for (int j=0; j<SIZE; j++) {
Point tmp = new Point(i, j);
if (tmp.i==point.i || tmp.j==point.j || tmp.id == point.id) {
if (tmp.i == point.i && tmp.j == point.j) return true;
if (board[tmp.i][tmp.j] == val) {
System.out.println(tmp.i +"|"+tmp.j);
return false;
}
}
}
}
return true;
}
public boolean recusive(char[][] board, Map<Point, Set<Character>> map, Iterator<Point> iterator) {
Point p = iterator.next();
System.out.println("not finished yet! ["+ p.i +"|"+ p.j + "]");
Set<Character> set = map.get(p);
for (char c : set) {
board[p.i][p.j] = c;
if (check(board, p, c) && (iterator.hasNext() && recusive(board, map, iterator))) {
return true;
} else {
board[p.i][p.j] = '.';
}
}
return false;
}
public void solveSudoku(char[][] board) {
//思路:降维法,将用‘。’表示的不可能性,变为1-9的概率数组
//1.建立可能矩阵,已有条件进入降维队列
//2.利用降维队列中内容,循环减少可能矩阵的可能性
//3.期间发现size为1的,降维,该点进入降维队列,作为下一轮的弹药
//如此循环直到所有点都降维了
Map<Point, Set<Character>> map = new HashMap<>();
Queue<Point> q = new LinkedList<>();
setInit(board, map, q);
//降维
while (!q.isEmpty()) {
demention(board, map, q);
}
//后来发现需要猜,所以到最后如果可能矩阵还是有未降维的,则需要逐一尝试;
//采用递归机制,从map取一个待定的点,遍历其set范围,当某值时,设置board,删map,然后递归下一步,发现不行要回退
//如果map中只有一个待定点了,遍历check就行了
while (map.size() > 0) {
System.out.println("not finished yet! " + map.size());
Iterator<Point> iterator = map.keySet().iterator();
recusive(board, map, iterator);
}
}
public static void main(String[] args) {
char[][] board = new char[][]{
{'.','.','9','7','4','8','.','.','.'},
{'7','.','.','.','.','.','.','.','.'},
{'.','2','.','1','.','9','.','.','.'},
{'.','.','7','.','.','.','2','4','.'},
{'.','6','4','.','1','.','5','9','.'},
{'.','9','8','.','.','.','3','.','.'},
{'.','.','.','8','.','3','.','2','.'},
{'.','.','.','.','.','.','.','.','6'},
{'.','.','.','2','7','5','9','.','.'}
};
Sudo solution = new Sudo();
solution.printBoard(board);
solution.solveSudoku(board);
solution.printBoard(board);
}
private void printBoard(char[][] board) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
System.out.print(board[i][j] + " ");
}
System.out.println();
}
}
}
后来终于找到了我在2013年写的c的数独解法(递归法)
在leetcode上同样编译测试通过
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <sys/timeb.h>
int shudu[10][10]; //保存数独,使用时下标从1开始
int tt[10][10][10]; //保存数独可能取值,数独每点对应这里一组
FILE* flog; //日志文件
FILE* fshudu; //读取数独题目所在文件
//给数独数组的有值节点赋值
int init_shudu(void);
//初始化数独数组对应的三维数组
int init_tt(int x, int y);
//打印数独和记运行时间
void print_shudu(void);
//数独求解函数,采用递归方式从上到下从左到右求解
int looper(int x, int y);
//判断数独某单元格值没有重复性
int no_repeat(int x, int y, int value);
int main(int argc, char* argv[])
{
int i, j, ret;
fopen_s(&flog, "mylog.txt","w");
printf("开始进行数独求解!----------------\n");
fprintf(flog, "开始进行数独求解!----------------\n");
ret = init_shudu();
if (ret != 0) goto Error;
ret = init_tt(0, 0);
if (ret != 0) goto Error;
print_shudu();
ret = looper(1, 1);
if (ret != 0) goto Error;
print_shudu();
//对解出的数独进行逐格校验
for (i=1; i<=9; i++)
for (j=1; j<=9; j++)
if (no_repeat(i, j, shudu[i][j]) != 0)
fprintf(flog, "检查发现单元[%d,%d]值不对!\n",i,j);
printf("数独求解结束!----------------\n");
fprintf(flog, "数独求解结束!----------------\n");
fclose(flog);
printf("请按回车键退出...");
getchar();
return 0;
Error:
fclose(flog);
printf("程序发生异常终止!请检查日志!\n");
printf("请按回车键退出...");
getchar();
return ret;
}
/**初始化数独数组(9*9 矩阵)有值单元格,无值则为0 */
int init_shudu(void) {
int i, j, num, cnt = 0;
for(i=1; i<=9; i++)
for(j=1; j<=9; j++) shudu[i][j] = 0;
if (fopen_s(&fshudu, "myshudu.txt","r") != 0) {
printf("打开数独文件报错!请检查myshudu.txt!\n");
fprintf(flog, "打开数独文件报错!请检查myshudu.txt!\n");
return -1;
}
for (i=1; i<=9; i++) {
for (j=1; j<=9; j++) {
cnt += fscanf_s(fshudu, "%d ", &num);
if (num<0 || num>9) return -1;
else shudu[i][j] = num;
}
}
if (cnt != 81) return -1;
fclose(fshudu);
return 0;
}
/**
@ 根据数独数组初始化对应的三维数组(除了传入参数点对应的数组)
@ 参数:保留点的坐标,该点对应的数组不被初始化;若要全初始化,传入[0,0]
*/
int init_tt(int x, int y) {
int i, j, k;
for(i=1; i<=9; i++)
for(j=1; j<=9; j++) {
if (x==i && y==j) continue;
if (shudu[i][j] != 0) for(k=1; k<=9; k++) tt[i][j][k] = -1;
else for(k=1; k<=9; k++) tt[i][j][k] = 0;
}
return 0;
}
/**数独求解函数,采用递归方式从上到下从左到右求解
@ 参数:当前节点在数独的坐标位置;
@ 返回值:0 成功 -1 无解 -2 位置错误
*/
int looper(int x, int y) {
int k, nexti, nextj;
//参数检查
if (x<1 || x>9 || y<1 || y>9) return -2;
//计算下一步的单元格的坐标
if (y==3 || y==6 || y==9) {
if (x == 9) { nexti = 1; nextj = y+1; }
else { nexti = x+1; nextj = y-2; }
} else { nexti = x; nextj = y+1; }
//单元格有初始值或已被赋值时
if (shudu[x][y] != 0) {
if (x==9 && y==9) {
printf("完成数独解算过程!\n");
return 0;
} else {
return looper(nexti, nextj);
}
}
//单元格没有值时需要进行试值
for (k=1; k<=9; k++) {
if (tt[x][y][k] == -1) continue;
else {
tt[x][y][k] = -1;
if (no_repeat(x, y, k) == 0) {
int tmpi;
shudu[x][y] = k;
fprintf(flog,"位置[%d,%d] := %d\n", x, y, k);
if (x==9 && y==9) {
printf("在对最后一格赋值后,完成数独解算过程!\n");
return 0;
}
//对所在的行、列筛除
for (tmpi=1; tmpi<=9; tmpi++) {
tt[x][tmpi][k] = -1;
tt[tmpi][y][k] = -1;
}
//对所在的3*3小方阵筛除
if (x<=3 && y<=3) { tt[1][1][k] = -1; tt[1][2][k] = -1; tt[1][3][k] = -1; tt[2][1][k] = -1; tt[2][2][k] = -1; tt[2][3][k] = -1; tt[3][1][k] = -1; tt[3][2][k] = -1; tt[3][3][k] = -1; }
else if (x<=3 && y<=6) { tt[1][4][k] = -1; tt[1][5][k] = -1; tt[1][6][k] = -1; tt[2][4][k] = -1; tt[2][5][k] = -1; tt[2][6][k] = -1; tt[3][4][k] = -1; tt[3][5][k] = -1; tt[3][6][k] = -1; }
else if (x<=3 && y<=9) { tt[1][7][k] = -1; tt[1][8][k] = -1; tt[1][9][k] = -1; tt[2][7][k] = -1; tt[2][8][k] = -1; tt[2][9][k] = -1; tt[3][7][k] = -1; tt[3][8][k] = -1; tt[3][9][k] = -1; }
else if (x<=6 && y<=3) { tt[4][1][k] = -1; tt[4][2][k] = -1; tt[4][3][k] = -1; tt[5][1][k] = -1; tt[5][2][k] = -1; tt[5][3][k] = -1; tt[6][1][k] = -1; tt[6][2][k] = -1; tt[6][3][k] = -1; }
else if (x<=6 && y<=6) { tt[4][4][k] = -1; tt[4][5][k] = -1; tt[4][6][k] = -1; tt[5][4][k] = -1; tt[5][5][k] = -1; tt[5][6][k] = -1; tt[6][4][k] = -1; tt[6][5][k] = -1; tt[6][6][k] = -1; }
else if (x<=6 && y<=9) { tt[4][7][k] = -1; tt[4][8][k] = -1; tt[4][9][k] = -1; tt[5][7][k] = -1; tt[5][8][k] = -1; tt[5][9][k] = -1; tt[6][7][k] = -1; tt[6][8][k] = -1; tt[6][9][k] = -1; }
else if (x<=9 && y<=3) { tt[7][1][k] = -1; tt[7][2][k] = -1; tt[7][3][k] = -1; tt[8][1][k] = -1; tt[8][2][k] = -1; tt[8][3][k] = -1; tt[9][1][k] = -1; tt[9][2][k] = -1; tt[9][3][k] = -1; }
else if (x<=9 && y<=6) { tt[7][4][k] = -1; tt[7][5][k] = -1; tt[7][6][k] = -1; tt[8][4][k] = -1; tt[8][5][k] = -1; tt[8][6][k] = -1; tt[9][4][k] = -1; tt[9][5][k] = -1; tt[9][6][k] = -1; }
else if (x<=9 && y<=9) { tt[7][7][k] = -1; tt[7][8][k] = -1; tt[7][9][k] = -1; tt[8][7][k] = -1; tt[8][8][k] = -1; tt[8][9][k] = -1; tt[9][7][k] = -1; tt[9][8][k] = -1; tt[9][9][k] = -1; }
if (looper(nexti, nextj) != 0) { //递归,如果下一步不成功则前功尽弃,需要回退
int tmpk;
shudu[x][y] = 0;
init_tt(x, y);
for (tmpk=k+1; tmpk<=9; tmpk++) tt[x][y][tmpk] = 0; //调试,回退本列上未尝试部分
} else return 0; //如果下一步成功,就跳出对k的遍历
} //对应 if (no_repeat(x, y, k) 的判断,如果这个试值不行,就去试下个值
} // if (tt[x][y][k]
} //for
return -1;
}
/**传入参数是当前节点在数独的位置,和试图的取值;
@ 注意:根据数独的规则,合法的取值应该满足行、列、小方阵都没有重复
@ 返回值:0 可以取该值 -1 不可取该值 -2 位置错误 -3 试图赋的值错误
*/
int no_repeat(int x, int y, int value) {
int tmp;
int ipos, jpos, m, n;
if (x<1 || x>9 || y<1 || y>9) return -2;
if (value<1 || value>9) return -3;
for (tmp=1; tmp<=9; tmp++)
if (y!=tmp && (shudu[x][tmp]==value)) return -1;
for (tmp=1; tmp<=9; tmp++)
if (x!=tmp && (shudu[tmp][y]==value)) return -1;
if (x==1 || x==4 || x==7) ipos = 0;
if (x==2 || x==5 || x==8) ipos = 1;
if (x==3 || x==6 || x==9) ipos = 2;
if (y==1 || y==4 || y==7) jpos = 0;
if (y==2 || y==5 || y==8) jpos = 1;
if (y==3 || y==6 || y==9) jpos = 2;
for (m=0; m<3; m++)
for (n=0; n<3; n++)
if ((m!=ipos) && (n!=jpos) && (shudu[x-ipos+m][y-jpos+n]==value))
return -1;
return 0;
}
void print_shudu(void) {
int i, j;
struct timeb tp1;
struct tm *s_tm1;
//打印这个数独题目
for(i=1; i<=9; i++) {
for(j=1; j<=9; j++) printf("%d ", shudu[i][j]);
printf("\n");
}
printf("---------------------\n");
//记录交易时间
ftime(&tp1);
s_tm1 = localtime(&(tp1.time));
printf("Start : %02d:%02d:%02d\n\n", s_tm1->tm_hour, s_tm1->tm_min, s_tm1->tm_sec);
}