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);
}