Java解數獨算法(非舞蹈鏈)

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