AcWing 166 Sudoku(9*9)

AcWing 166 Sudoku(9 * 9)

题目描述:

数独是一种传统益智游戏,你需要把一个 9×9 的数独补充完整,使得图中每行、每列、每个 3×3 的九宫格内数字 1∼9均恰好出现一次。请编写一个程序填写数独。

输入样例

4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......
......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.
end

输出样例

417369825632158947958724316825437169791586432346912758289643571573291684164875293
416837529982465371735129468571298643293746185864351297647913852359682714128574936

题目分析

题目明显要使用DFS搜索,只需要对每个空格位置依次选择可以放入的数字,直到遍历完整张图为止,然而,数度问题的搜索树十分庞大,直接盲目搜索会导致超时,此时需要引入一些优化手段。

  1. 如果让你来完成这个数独,你会从那里开始入手?

    很明显,我们一定会选择可以填入数字数量最少的格子开始枚举,这样更有可能得到最优解,而不是不停失败回溯。因此,我们的程序也可以从这个方向入手。

  2. 在搜索程序中,影响时间效率的因素除了搜索树的规模(时间复杂度)之外,还有在每个状态结点上的纪律、检索、更新的开销(常数)。我们可以使用位运算代替数组执行“对数独各个位置”所填数字的纪律和检查与统计,这就是所说的”常数优化“。具体方案:

    • 对于每行、每列、每个九宫格,用一个九位二进制数(全局整数变量)保存哪些数字可以被填(1表示当前数字可填,反之不可)。
    • 如何求出某个位置有那些数字可填?只需要对行、列、九宫格作交集运算即可(&运算)。
    • 如何快速取出每个可选的数字?用lowbit运算,再用数组存储对应二进制数所代表的数字,可以用O(1)的时间取出每个可选数字。
    • 在某个位置填入一个数字后,只需要把该位置对应的行、列、九宫格记录的二进制位改为0即可更新状态,回溯只需要改为1还原现场。
    • 采用二进制优化后,每个状态可选数字数量就是二进制中1的数量,我们可以设一个ones数组记录每个数字二进制中1的数量,就可以用O(1)的时间取出该数量。

代码如下:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 9;

int ones[1 << N], map[1 << N]; // ones记录每个数字二进制中有多少个1,map记录每个二进制下的1对应的数字
int row[N], line[N], cell[3][3]; // 用二进制来存储每行/列/九宫格的可选状态
char str[100];

inline int lowbit (int x) { return x & (-x); }

void init () {
    // 一开始,所有状态都为'.',所有数都可以选择
    for (int i = 0; i < N; i++) row[i] = line[i] = (1 << N) - 1;
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            cell[i][j] = (1 << N) - 1;
}

// 求出(x, y)位置的可选状态
inline int get (int x, int y) {
    return row[x] & line[y] & cell[x / 3][y / 3];
}

bool dfs (int cnt) {
    if (cnt == 0) return true;
    
    // 找出可选方案数最小的格子
    int minv = 10;
    int x, y;
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++)
            if (str[i * 9 + j] == '.') {
                int t = ones[get(i, j)]; // 可选方案的交集
                if (t < minv) {
                    minv = t;
                    x = i, y = j;
                }
            }
    
    for (int i = get(x, y); i; i -= lowbit(i)) {
        int t = map[lowbit(i)];
        row[x] -= 1 << t;
        line[y] -= 1 << t;
        cell[x / 3][y / 3] -= 1 << t;
        str[x * 9 + y] = '1' + t;
        if (dfs(cnt - 1)) return true;
        row[x] += 1 << t;
        line[y] += 1 << t;
        cell[x / 3][y / 3] += 1 << t;
        str[x * 9 + y] = '.';
    }
    
    return false;
}

int main () {
    // 预处理1
    for (int i = 0; i < 1 << N; i++) {
        int s = 0;
        for (int j = i; j; j -= lowbit(j)) s++;
        ones[i] = s;
    }
    // 预处理2
    for (int i = 0; i < N; i++) map[1 << i] = i;
    
    while (cin >> str, str[0] != 'e') {
        // 预处理3
        init ();
        // 根据实际给定的数据,更新每行/列/九宫格的可选状态集
        int cnt = 0; // 需要填入的空格
        for (int i = 0, k = 0; i < N; i++) {
            for (int j = 0; j < N; j++, k++) {
                if (str[k] != '.') {
                    int t = str[k] - '1'; // 把1~9印射成0~8(方便位运算)
                    row[i] -= 1 << t;
                    line[j] -= 1 << t;
                    cell[i / 3][j / 3] -= 1 << t;
                }
                else cnt++;
            }
        }
        // 对剩下需要填入的cnt个格子搜索
        dfs(cnt);
        cout << str << endl;
    }
    return 0;
}
posted @ 2021-07-14 09:08  Horb7  阅读(43)  评论(0编辑  收藏  举报