前言:這是一篇算法題的AC代碼。
我首先是在leetcode上刷到這道題,然而用例過少,因此在51nod上又做了一遍。作爲Java代碼,運行時間大約在900ms,效率無疑是十分低下的。
然而想要更進一步的優化,就只能選擇舞蹈鏈,我尚未讀懂……嚴格來說,思路是讀懂了,代碼還沒有……所以先貼上這份AC代碼作爲日記,其中註釋很詳細,內置了若干數據,歡迎大神指點。
原題:51nod 題號1211 解數獨。
import java.io.*;
import java.util.*;
// 本方案充分利用整數的數位進行判斷,從而可以簡單地通過一次合併邏輯與得到可選數值
// 但是在數據檢測的重複性上處理存在重大缺失,儘管通過廉價的布爾值判斷可以過濾掉
// 無意義的大量的重複的判定依然消費了太多計算量
// 接下來要做的事情就是讀懂舞蹈鏈解數獨的真正算法
public class Solution {
// 解決方案
List<Integer> backdate = new ArrayList<>();
int[][] board;
public boolean solveSudoku(int[][] board) {
this.init(board);
if(this.tryBack(0) != 1) {
return false;
}
return true;
}
// 主遞歸函數,即便不增加剪枝函數也一樣能完成任務,不過效率會低很多
private int tryBack(int index) {
if(index==backdate.size()) {
// 遞歸到列的終點說明成功解得一個數組
this.loadBoard();
return 1;
}
// e 是座標約定,其二進制最後四位表示j,五到八位表示i
int e = backdate.get(index);
if(isExisted(e)) return tryBack(index+1);
// 數獨中可選數值的數位表示
int selection = this.get(e);
// 約定 t 是數位表示轉化爲實值的結果,記錄爲0~8
int t = 0;
boolean hasAnswer = false;
while(selection>0) {
// 末位爲0意味着這個數不可選
if((selection&1)==0) {
selection >>= 1;
t++;
continue;
}
// 嘗試選中當前值
this.push(e, t);
// 嘗試剪枝
if(!this.fastScaning(e)) {
// 剪枝失敗時重置判定集已經在剪枝函數中完成,這裏只需要重置當前選中值
this.pop(e);
selection >>= 1;
t++;
continue;
}
// 嘗試遞歸
int tryNext = tryBack(index+1);
// 根據提議要求,多解、無解的情況都返回 No Solution 當存在多解時無需再對數據做任何處理
if(tryNext == -1) return -1;
if(tryNext == 1 && hasAnswer) return -1;
if(tryNext == 1) hasAnswer = true;
// 重置判定集,進入下一次嘗試
this.clearResults();
this.pop(e);
selection >>= 1;
t++;
}
return hasAnswer ? 1 : 0;
}
// 剪枝成功時將這一批數據壓入棧,主遞歸函數結束當前節點時需要通過棧讀出數據並重置狀態
private boolean fastScaning(int e) {
int i = e>>4;
int j = e&15;
List<Integer> marks = new ArrayList<>();
if(this.fastScaning(i, j, marks)) {
scanStack.push(marks);
return true;
} else {
for(int e_ : marks) {
this.pop(e_);
}
return false;
}
}
// 快速剪枝,當座標可選數字爲0時,這是失敗的數據。否則如果是1,則將其填充進去。
private boolean fastScaning(int i, int j, List<Integer> marks) {
int[] xs = new int[21];
int[] ys = new int[21];
this.findNexts(xs, ys, i, j);
for(int p=0; p<21; p++) {
int x = xs[p];
int y = ys[p];
if(isExisted(x, y)) {
continue;
}
int vals = this.get(x, y);
int n = this.count(vals);
if(n == 0) {
return false;
} else if(n == 1) {
int t = this.convert(vals);
marks.add((x<<4)|y);
this.push(x, y, t);
if(!fastScaning(x, y, marks)) {
return false;
}
}
}
return true;
}
// 失敗的處理邏輯 -> 意圖通過當前座標尋找行、列、九宮作爲"接下來要尋找的數據" -> 卻導致了大量的重複判定,舞蹈鏈則不存在這個問題
int[][] incolrow = {{3,4,5,6,7,8}, {0,1,2,6,7,8}, {0,1,2,3,4,5}};
int[][] insqr = {{0,1,2}, {3,4,5}, {6,7,8}};
private void findNexts(int[] xs, int[] ys, int i, int j) {
for(int x=0; x<6; x++) {
xs[x] = i;
ys[x] = incolrow[i/3][x];
}
for(int y=0; y<6; y++) {
xs[y+6] = incolrow[j/3][y];
ys[y+6] = j;
}
for(int x=0; x<3; x++) {
for(int y=0; y<3; y++) {
xs[(x*3+y)+12] = insqr[i/3][x];
ys[(x*3+y)+12] = insqr[j/3][y];
}
}
}
// 數據邏輯區
int[] row = new int[9];
int[] col = new int[9];
int[] sqr = new int[9];
// 返回當前i,j可選數字的數位表示
private int get(int e) {
return this.get(e>>4, e&15);
}
private int get(int i, int j) {
return row[i] & col[j] & sqr[(i/3*3)+(j/3)];
}
// 將給定數值加入到當前座標的判定集中
private void push(int e, int num) {
int i = e>>4;
int j = e&15;
this.push(i, j, num);
}
private void push(int i, int j, int num) {
int b = ~(1<<num);
row[i] &= b;
col[j] &= b;
sqr[(i/3*3)+(j/3)] &= b;
this.addResult(i, j, num);
}
// 從判定集中清除當前座標值
private void pop(int e) {
int i = e>>4;
int j = e&15;
int b = (1<<this.findResult(i, j));
row[i] |= b;
col[j] |= b;
sqr[(i/3*3)+(j/3)] |= b;
this.delResult(i, j);
}
// 初始化工作
private void init(int[][] board) {
this.board = board;
for(int p=0;p<9;p++) {
row[p] = 0x1FF;
col[p] = 0x1FF;
sqr[p] = 0x1FF;
}
results = new int[9][];
for(int i=0;i<9;i++) {
results[i] = new int[9];
for(int j=0;j<9;j++) {
int e = (i<<4)|j;
if(board[i][j] == 0) {
backdate.add(e);
} else {
this.push(e, board[i][j]-1);
}
}
}
this.sort(backdate);
}
// 結果判定標識區
int[][] results;
Stack<List<Integer>> scanStack = new Stack<>();
private boolean isExisted(int e) {
return this.isExisted(e>>4, e&15);
}
private boolean isExisted(int i, int j) {
return results[i][j]!=0;
}
private void addResult(int i, int j, int value) {
results[i][j] = value+1;
}
private int findResult(int i, int j) {
return results[i][j] - 1;
}
private void delResult(int i, int j) {
results[i][j] = 0;
}
private void clearResults() {
List<Integer> marks = scanStack.pop();
for(int e : marks) {
this.pop(e);
}
}
private void loadBoard() {
for(int i=0; i<9; i++) {
for(int j=0; j<9; j++) {
board[i][j] = results[i][j];
}
}
}
// 工具區
// 對待處理列標進行排序,數據量足夠小,採用冒泡排序即可
private void sort(List<Integer> backdate) {
for(int i=0; i<backdate.size(); i++) {
for(int j=backdate.size()-1; j>i; j--) {
int a = backdate.get(j-1);
int b = backdate.get(j);
if(bigThan(a, b)) {
backdate.set(j-1, b);
backdate.set(j, a);
}
}
}
}
private boolean bigThan(int sit1, int sit2) {
return this.count(get(sit1)) > this.count(get(sit2));
}
// 標準函數,統計整數在二進制位上有幾個1
private int count(int m) {
int temp = m - ((m>>1)&033333333333) - ((m>>2)&011111111111);
return ((temp + (temp>>3)) & 030707070707) % 63;
}
// 當數位上有且只有一個1時,其位數恰好表示數獨中唯一可選的值
private int convert(int p2n) {
int t = 0;
while(p2n!=1) {
t++;
p2n>>=1;
}
return t;
}
// 主函數處理區
private static void readsudo(int[][] sudo, Scanner scan) {
for(int i=0; i<9; i++) {
sudo[i] = new int[9];
for(int j=0; j<9; j++) {
sudo[i][j] = scan.nextInt();
}
}
}
private static void printSudo(int[][] sudo) throws Exception {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out), 1 << 16);
for(int[] cs : sudo) {
for(int c : cs) {
writer.write(c + " ");
}
writer.write("\r\n");
}
writer.flush();
}
public static void main(String args[]) throws Exception {
/*
String data =
"0 6 0 5 9 3 0 0 0\n"+
"9 0 1 0 0 0 5 0 0\n"+
"0 3 0 4 0 0 0 9 0\n"+
"1 0 8 0 2 0 0 0 4\n"+
"4 0 0 3 0 9 0 0 1\n"+
"2 0 0 0 1 0 6 0 9\n"+
"0 8 0 0 0 6 0 2 0\n"+
"0 0 4 0 0 0 8 0 7\n"+
"0 0 0 7 8 5 0 1 0";
*/
/*
String data =
"0 0 0 3 4 0 0 0 0\n"+
"8 0 0 0 0 0 0 2 0\n"+
"0 7 0 0 1 0 5 0 0\n"+
"4 0 0 0 0 5 3 0 0\n"+
"0 1 0 0 7 0 0 0 6\n"+
"0 0 3 2 0 0 0 8 0\n"+
"0 6 0 5 0 0 0 0 9\n"+
"0 0 4 0 0 0 0 3 0\n"+
"0 0 0 0 0 9 7 0 0";
*/
/*
String data =
"0 0 0 0 0 0 0 0 0\n"+
"0 0 0 0 0 0 0 0 0\n"+
"0 0 0 0 0 0 0 0 0\n"+
"0 0 0 0 0 0 0 0 0\n"+
"0 0 0 0 0 0 0 0 0\n"+
"0 0 0 0 0 0 0 0 0\n"+
"0 0 0 0 0 0 0 0 0\n"+
"0 0 0 0 0 0 0 0 0\n"+
"0 0 0 0 0 0 0 0 0";
*/
/*
String data =
"0 0 0 4 0 0 0 0 7\n"+
"0 1 0 0 0 0 0 0 0\n"+
"0 0 0 0 2 0 0 0 0\n"+
"6 0 0 0 5 0 2 7 0\n"+
"0 0 0 0 0 0 8 3 0\n"+
"7 0 0 0 0 0 0 0 0\n"+
"2 0 6 0 0 0 5 0 0\n"+
"0 0 0 1 0 0 0 0 4\n"+
"0 0 0 3 0 0 0 0 0";
*/
/*
String data =
"0 6 0 5 9 3 0 0 0\n"+
"9 0 1 0 0 0 5 0 0\n"+
"0 3 0 4 0 0 0 9 0\n"+
"1 0 8 0 2 0 0 0 4\n"+
"4 0 0 3 0 9 0 0 1\n"+
"2 0 0 0 1 0 6 0 9\n"+
"0 8 0 0 0 6 0 2 0\n"+
"0 0 4 0 0 0 8 0 7\n"+
"0 0 0 7 8 5 0 1 0";
*/
/*
String data =
"7 6 2 5 0 3 1 4 8\n"+
"9 4 1 0 0 8 5 3 6\n"+
"8 3 5 4 0 0 7 9 2\n"+
"1 9 8 6 2 0 3 5 4\n"+
"4 7 6 3 5 0 2 8 1\n"+
"2 5 3 8 1 0 6 7 9\n"+
"3 8 7 1 4 0 9 2 5\n"+
"5 1 4 9 3 0 8 6 7\n"+
"6 2 9 7 8 0 4 1 3";
*/
// 51nod 題號 1211 數據格式
Scanner scan = new Scanner(new ByteArrayInputStream(data.getBytes()));
int[][] sudo = new int[9][];
readsudo(sudo, scan);
if(new Solution().solveSudoku(sudo)) {
printSudo(sudo);
} else {
System.out.println("No Solution");
}
}
}