解數獨(Leetcode-37 / HDU-1426)/回溯/狀態壓縮
阿新 • • 發佈:2020-09-18
解數獨(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;
};