解数独(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;
};