解数独(Leetcode-37 / HDU-1426)/回溯/状态压缩

解数独(Leetcode-37 / HDU-1426)/回溯/状态压缩

问题描述

编写一个程序,通过填充空格来解决数独问题。

  • 一个数独的解法需遵循如下规则:
    • 数字 1-9 在每一行只能出现一次。
    • 数字 1-9 在每一列只能出现一次。
    • 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
    • 空白格用 '.' 表示。

img

一个数独。

img

答案被标成红色。

提示:

给定的数独序列只包含数字 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;
};
posted @ 2020-09-18 20:33  退役的熬夜选手  阅读(244)  评论(0编辑  收藏  举报