1. 程式人生 > 實用技巧 >解數獨(Leetcode-37 / HDU-1426)/回溯/狀態壓縮

解數獨(Leetcode-37 / HDU-1426)/回溯/狀態壓縮

解數獨(Leetcode-37 / HDU-1426)/回溯/狀態壓縮

問題描述

編寫一個程式,通過填充空格來解決數獨問題。

  • 一個數獨的解法需遵循如下規則:
    • 數字 1-9 在每一行只能出現一次。
    • 數字 1-9 在每一列只能出現一次。
    • 數字 1-9 在每一個以粗實線分隔的 3x3 宮內只能出現一次。
    • 空白格用 '.' 表示。

一個數獨。

答案被標成紅色。

提示:

給定的數獨序列只包含數字 1-9 和字元 '.' 。
你可以假設給定的數獨只有唯一解。
給定數獨永遠是 9x9 形式的。


DFS+回溯

對於每個格子,只要沒有填寫數字就進行深搜,判斷1-9中哪個可以填入

填入後繼續深搜然後回溯

這樣可以遍歷到所有可能的結果

void dfs(int step) {	//step為當前所對應選的步數
    if(step==cnt) {
        // 如果cnt個點都已經找完了
        for(int i = 0;i < 9;i++) {
            for(int j = 0;j < 9;j++) {
                cout << mp[i][j] << " ";
            }
            cout << endl;
        }
        return;
    }
    for(int i = 1;i <= 9;i++) {
        if(check(step,i)) {	
            // 判重 + 剪枝 (判斷i是否可以填入)
            mp[node[step].x][node[step].y] = i;
            dfs(step+1);
            // 回溯
            mp[node[step].x][node[step].y] = 0;	
        }
    }
    return;
}

判重 + 剪枝

判斷行列以及3x3格子中有無重複

對3x3方格內有無重複進行判斷

如果當前的座標為 ( i , j ) , 令x=i/3x3,y=j/3x3

則點(x , y)為該點所對應的3x3格子的最左上角的點, 遍歷即可

// 對行列重複元素進行判斷
for(int i = 0;i < 9;i++) {
    // 判斷行和列中是否有重複
    if(mp[node[step].x][i]==k||mp[i][node[step].y]==k) 
        return false;
}
// 對該元素所在方格進行判斷
int n = node[step].x/3*3;   //
int m = node[step].y/3*3;   //
for(int i = n;i < n+3;i++) {
    for(int j = m;j < m+3;j++) {
        if(mp[i][j]==k) return false;
    }
}

完整程式碼

#include <iostream>
using namespace std;
// 存圖
int mp[10][10];
char temp;
int cnt;int flag = 0;
struct {
    // ?點的座標
    int x,y;
} node[100];
bool check(int step,int k) {
    // 判斷數字k能否用在node[k]中
    for(int i = 0;i < 9;i++) {
        // 判斷行和列中是否有重複
        if(mp[node[step].x][i]==k||mp[i][node[step].y]==k) 
            return false;
    }
    // 判斷3x3方塊中是否存在
    int n = node[step].x/3*3;   //行
    int m = node[step].y/3*3;   //列
    for(int i = n;i < n+3;i++) {
        for(int j = m;j < m+3;j++) {
            if(mp[i][j]==k) return false;
        }
    }
    return true;
}
void dfs(int step) {
    if(step==cnt) {
        for(int i = 0;i < 9;i++) {
            for(int j = 0;j < 9;j++) {
                cout << mp[i][j] << " ";
            }   //注意這裡(格式-暫定)
            cout << endl;
        }
        return;
    }
    for(int i = 1;i <= 9;i++) {
        if(check(step,i)) {
            mp[node[step].x][node[step].y] = i;
            dfs(step+1);
            mp[node[step].x][node[step].y] = 0;
        }
    }
    return;
}
int main()
{
    freopen("in.txt","r",stdin);
    while(cin >> temp) {
        cnt=0;
        if(temp=='?') {
            node[cnt].x = 0;
            node[cnt++].y = 0;
            mp[0][0] = 0;
        } else {
            mp[0][0] = temp-'0';
        }
        for(int i = 0;i < 9;i++) {
            for(int j = 0;j < 9;j++) {
                if(i==0&&j==0)  continue;
                cin >> temp;
                if(temp=='?') {
                    node[cnt].x = i;
                    node[cnt++].y = j;
                    mp[i][j] = 0;
                } else {
                    mp[i][j] = temp-'0';
                }
            }
        }
        if(flag)    cout << endl;
        dfs(0);
    }
    return 0;
}

hdu 1426 樣例

輸入

7 1 2 ? 6 ? 3 5 8
? 6 5 2 ? 7 1 ? 4
? ? 8 5 1 3 6 7 2
9 2 4 ? 5 6 ? 3 7
5 ? 6 ? ? ? 2 4 1
1 ? 3 7 2 ? 9 ? 5
? ? 1 9 7 5 4 8 6
6 ? 7 8 3 ? 5 1 9
8 5 9 ? 4 ? ? 2 3

輸出

7 1 2 4 6 9 3 5 8
3 6 5 2 8 7 1 9 4
4 9 8 5 1 3 6 7 2
9 2 4 1 5 6 8 3 7
5 7 6 3 9 8 2 4 1
1 8 3 7 2 4 9 6 5
2 3 1 9 7 5 4 8 6
6 4 7 8 3 2 5 1 9
8 5 9 6 4 1 7 2 3

狀態壓縮

用位集表示狀態

用位集(bitset)表示某一個點(x,y)的狀態, 即點(x,y)可以填那些數

row表示每一行狀態, col表示每一列狀態, cell表示3x3格子中的狀態

例如: row[2] = 110101011

每一位對應的數字 123456789

表示 第三行(注意下標)已經填了1,2,4,6,8,9 則它可能填的狀態為~row[2] (當然這裡僅對行進行了討論)

此外, 如果 col[5] = 111101111 代表第六列已經填了1,2,3,4,6,7,8,9

則 row[2] | col[5] = 111101111 代表第三行和第六列相交的那個點只能填入5 (要取反~)

下面的註釋寫的很詳細了!

using namespace std;
class Solution {
public:
    bitset<9> getPossibleStatus(int x, int y) {
        // 位運算 對 點(x,y) 所在的行和列和格子進行壓縮  返回所得可能填的值
        // 注意取反
        return ~(rows[x] | cols[y] | cells[x / 3][y / 3]);  
    }

    vector<int> getNext(vector<vector<char>>& board) {
        //  每次都使用都選擇能填的數字最少的格子開始填
        vector<int> ret;
        int minCnt = 10;
        for (int i = 0; i < board.size(); i++) {
            for (int j = 0; j < board[i].size(); j++) {
                // 不是要填的直接continue
                if (board[i][j] != '.') continue;
                // 當前點能填的狀態的壓縮
                auto cur = getPossibleStatus(i, j);
                // 要找到狀態最少的哪個
                if (cur.count() >= minCnt) continue;
                ret = { i, j };
                minCnt = cur.count();
            }
        }
        // 返回的是一個點
        return ret;
    }

    void fillNum(int x, int y, int n, bool fillFlag) {
        // 根據fillFlag 將n填入點(x,y)
        // fillFlag為true為填入 ,, fillFlag為false為消除(回溯)
        rows[x][n] = (fillFlag) ? 1 : 0;
        cols[y][n] = (fillFlag) ? 1 : 0;
        cells[x/3][y/3][n] = (fillFlag) ? 1: 0;
    }
    
    bool dfs(vector<vector<char>>& board, int cnt) {
        // cnt為0時,所有的 . 都已經填完
        if (cnt == 0) return true;
        // 尋找最優狀態準備填入
        auto next = getNext(board);
        // 解出所有可能狀態
        auto bits = getPossibleStatus(next[0], next[1]);
        for (int n = 0; n < bits.size(); n++) {
            // 對所有可能狀態進行嘗試
            // 為0說明該位已被填入   不做考慮
            // bitset::test()是C++ STL中的一個內建函式,用於測試是否設定了給定索引處的位。
            if (!bits.test(n)) continue;
            // 測試 將 n 填入填入點(next[0], next[1])
            fillNum(next[0], next[1], n, true);
            board[next[0]][next[1]] = n + '1';  // n 取值為0-8  
            // dfs
            if (dfs(board, cnt - 1)) return true;
            // 回溯
            board[next[0]][next[1]] = '.';
            fillNum(next[0], next[1], n, false);
        }
        return false;
    }

    void solveSudoku(vector<vector<char>>& board) {
        // 行對應的狀態
        rows = vector<bitset<9>>(9, bitset<9>());
        // 列對應的狀態
        cols = vector<bitset<9>>(9, bitset<9>());
        // 3x3 方格對應的狀態
        cells = vector<vector<bitset<9>>>(3, vector<bitset<9>>(3, bitset<9>()));
        // cnt為要填的總數
        int cnt = 0;
        for (int i = 0; i < board.size(); i++) {
            for (int j = 0; j < board[i].size(); j++) {
                cnt += (board[i][j] == '.');
                if (board[i][j] == '.') continue;
                // 為何 減去 1?   如果例如board[i][j]為 '1' 那麼減去 '1' 即為 整型0 對應著第一位(一共九位),即不用移動,表示1
                int n = board[i][j] - '1';
                // 索引零對應的是儲存的最後一位,cout輸出和按照索引輸出會得到互為倒序的結果~
                rows[i] |= (1 << n);
                cols[j] |= (1 << n);
                cells[i / 3][j / 3] |= (1 << n);c
            }
        }
        dfs(board, cnt);
    }
private:
    vector<bitset<9>> rows;
    vector<bitset<9>> cols;
    vector<vector<bitset<9>>> cells;
};