Leetcode37題,解數獨

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);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章