【算法专题】数独问题详解

数独问题详解

1. 概述

根据百度百科的描述:数独(shù dú)是源自18世纪瑞士的一种数学游戏。是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个九宫格内的数字均含1-9,不重复。

  • 下面给出数独问题的不同解法,从最简单的暴力开始,然后使用二进制进行优化,最后采用Dancing Links进行优化。

  • 下面给出三个题目:

    • 第一个题目使用最暴力的解法;

    • 第二个题目使用二进制优化;

    • 最后一个题目使用两种优化方式求解,一种是二进制优化,另一种是Dancing Links

2. 题目

AcWing 1613. 数独简单版

问题描述

分析

  • 可以开辟三个判重数组,分别存储行、列、九宫格已经有哪些数字被填写了。这里九宫格从左向右,从上向下标号依次是(0, 0)~(2, 2),对于某个坐标(i, j),其所在的九宫格编号为(i/3, j/3)

  • 暴搜所有方案,然后判断是否合法即可。从左上角向右下角搜索,一个格子一个格子的考虑,每次考虑完一行,跳到下一行即可。

代码

  • C++
#include <iostream>

using namespace std;

const int N = 10;

char g[N][N];
bool row[N][N], col[N][N], cell[3][3][N];

bool dfs(int x, int y) {
    
    if (y == 9) return dfs(x + 1, 0);
    if (x == 9) {
        for (int i = 0; i < 9; i++) puts(g[i]);
        return true;
    }
    
    if (g[x][y] != '.') return dfs(x, y + 1);
    
    for (int i = 1; i <= 9; i++)
        if (!row[x][i] && !col[y][i] && !cell[x / 3][y / 3][i]) {
            row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = true;
            g[x][y] = i + '0';
            if (dfs(x, y + 1)) return true;
            g[x][y] = '.';
            row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = false;
        }
   	return false;
}

int main() {
    
    for (int i = 0; i < 9; i++) {
        scanf("%s", g[i]);
        for (int j = 0; j < 9; j++)
            if (g[i][j] != '.') {
                int x = g[i][j] - '0';
                row[i][x] = col[j][x] = cell[i / 3][j / 3][x] = true;
            }
    }
    
    dfs(0, 0);
    
    return 0;
}

AcWing 166. 数独

问题描述

分析

  • 我们可以依次填写每个空格,枚举所有情况即可。

  • 考虑如何剪枝:

    (1)优化搜索顺序:我们应该分支较少的格子,即可填数字较少的格子。

    (2)排除等效冗余:无

    (3)可行性剪枝:当前空位中所填写的内容不能与所在行、列、九宫格有重复数组。

    (4)最优性剪枝:无。

  • 另外我们还可以使用位运算优化:

    • 我们可以使用一个9位的二进制数据表示某一行或者某一列或者某个九宫格的状态,二进制位是0代表可以该行中已经有该数据了,比如0b110100000代表该行(或者该列、或者该九宫格)还可以填写9或者8或者6。
  • 求解二进制中最右侧的1可以使用lowbit操作。

代码

  • C++
#include <iostream>

using namespace std;

const int N = 9, M = 1 << N;

int ones[M], map[M];  // ones[i]表示i二进制表示中1的个数; map[i]表示log2(i)
int row[N], col[N], cell[3][3];  // 二进制优化
char str[100];

// 将row, col, cell二进制标识全部置为1,代表可以放置数字
void init() {
    for (int i = 0; i < N; i++)
        row[i] = col[i] = (1 << N) - 1;
    
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            cell[i][j] = (1 << N) - 1;
}

// is_set = true的话,在(x, y)放置上数字t,否则取消放置
void draw(int x, int y, int t, bool is_set) {
    if (is_set) str[x * N + y] = '1' + t;
    else str[x * N + y] = '.';
    
    int v = 1 << t;
    if (!is_set) v = -v;
    
    row[x] -= v;
    col[y] -= v;
    cell[x / 3][y / 3] -= v;
}

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

// 获取(x, y)位置可以放置的数据
int get(int x, int y) {
    return row[x] & col[y] & cell[x / 3][y / 3];
}

// cnt: 目前剩余需要填写位置的数量
bool dfs(int cnt) {
    
    if (!cnt) 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 * N + j] == '.') {
                int state = get(i, j);
                if (ones[state] < minv) {
                    minv = ones[state];
                    x = i, y = j;
                }
            }
    
    int state = get(x, y);
    for (int i = state; i; i -= lowbit(i)) {
        int t = map[lowbit(i)];
        draw(x, y, t, true);
        if (dfs(cnt - 1)) return true;
        draw(x, y, t, false);
    }
    
    return false;
}

int main() {
    
    for (int i = 0; i < N; i++) map[1 << i] = i;
    for (int i = 0; i < 1 << N; i++)
        for (int j = 0; j < N; j++)
            ones[i] += i >> j & 1;
    
    while (cin >> str, str[0] != 'e') {
        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';
                    draw(i, j, t, true);
                }
                else cnt++;

        dfs(cnt);
        
        puts(str);
    }
    
    return 0;
}

AcWing 169. 数独2

问题描述

分析

  • 存在两种解法:

解法一

  • 搜索顺序:每次找到一个空格,枚举空格选择哪个字母,然后往下递归。当所有空格都被填完,说明找到一个合法答案。

  • 优化:

    • (1)对于每个空格:如果不能填写任何一个字母,无解;如果只能填写一个字母,那么直接填上;

    • (2)对于每一行:如果某个字母不能出现在任何位置,无解;如果某个字母只有一个位置可以填写,则直接填上;

    • (3)对于每一列,和行类似;

    • (4)对于每个16宫格,和行类似;

    • (5)每次选择空格填写时,选择被选方案最少的格子来填。

解法二

  • 使用DancingLinks求解,需要将问题转化为精确覆盖问题。

  • 首先需要将该问题转化为一个01矩阵,行代表选择,列代表限制。

  • 这里的行表示:每个格子选择哪个数(用0~15表示A~P)。一共 1 6 2 16 ^ 2 162 个格子,每个格子16种选择,因此需要 1 6 3 16 ^ 3 163 行。

  • 列表示:

    • (1)每个格子恰好填一个数;占用 1 1 1 ~ 1 6 2 16^2 162 列;

    • (2)每行0~15恰好出现一次;占用 1 6 2 + 1 16^2+1 162+1 ~ 1 6 2 × 2 16^2 \times 2 162×2 列;

    • (3)每列0~15恰好出现一次;占用 1 6 2 × 2 + 1 16^2 \times 2+1 162×2+1 ~ 1 6 2 × 3 16^2 \times 3 162×3 列;

    • (4)每个方格0~15恰好出现一次;占用 1 6 2 × 3 + 1 16^2 \times 3+1 162×3+1 ~ 1 6 2 × 4 16^2 \times 4 162×4 列;

  • 对于第(4)个限制,我们从左到右,从上到下给每个方格的编号为0~15

  • 每一行我们只需要填写四个1,因此总共1的数量为 1 6 3 × 4 16 ^ 3 \times 4 163×4,这里的节点数目上限N=20000即可。

代码

  • C++
// 解法一:加入各种优化
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 16, M = N * N + 1;

int ones[1 << N];  // 存储每个数的二进制表示中有多少个1,例如ones[5] = 2
int log[1 << N];  // 存储数据的log值, log[1<<i] = i
int state[N][N];  // 每个空格的可以填写的字母(A~P),用二进制表示,二进制是1表示可选,最低位代表A
char str[N][N + 1];  // 存储数独的初始状态

int bstate[M][N][N], bstate2[M][N][N];  // 回溯(恢复现场)的时候使用
char bstr[M][N][N + 1];  // 棋盘状态的备份,回溯的时候使用

inline int lowbit(int x) {  // inline:加速调用过程
    return x & -x;
}

void draw(int x, int y, int c) {  // 在str[i][j]中填写字母(c + 'A')
    str[x][y] = c + 'A';
    // 更新行、列
    for (int i = 0; i < N; i++) {
        state[x][i] &= ~(1 << c);  // 将第c位置零
        state[i][y] &= ~(1 << c);
    }
    // 更新十六宫格
    int sx = x / 4 * 4, sy = y / 4 * 4;  // 十六宫格的左上角
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++)
            state[sx + i][sy + j] &= ~(1 << c);
    }
    // 更新(x, y)这个格子所代表的的状态
    state[x][y] = 1 << c;
}

bool dfs(int cnt) {

    if (!cnt) return true;
    /**
     * 枚举的过程
     */
    // 存储此时的状态,方便恢复现场
    int kcnt = cnt;
    memcpy(bstate[kcnt], state, sizeof state);
    memcpy(bstr[kcnt], str, sizeof str);  // 存储当前填写的方案

    // 对于每个空格,如果一个字母都不能填,则无解;如果只能填写一个字母,则直接填
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++)
            if (str[i][j] == '-') {
                int s = state[i][j];
                if (!s) {  // 说明一个字母都不能填写
                    memcpy(state, bstate[kcnt], sizeof state);
                    memcpy(str, bstr[kcnt], sizeof str);
                    return false;
                }
                if (ones[s] == 1) {
                    draw(i, j, log[s]);
                    cnt--;
                }
            }
    }

    // 对于每一行,如果某个字母不能出现在任何一个位置,则无解;
    // 如果某个字母,只能填在一个位置上,则直接填
    for (int i = 0; i < N; i++) {
        int sor = 0;  // sor中存储的是当前这一行可以填写的字母的并集,如果二进制表示不是16个1,说明不合法
        int sand = (1 << N) - 1;  // 某一位如果是1,表示这个字母只能在这一行出现一次
        int drawn = 0;  // 当前这一行已经填上的格子是哪些字母
        for (int j = 0; j < N; j++) {
            int s = state[i][j];
            sand &= ~(sor & s);  // s & sor的结果中如果某一位是1,表示某个字母可以在该行中填写两次
            sor |= s;
            if (str[i][j] != '-') drawn |= state[i][j];
        }
        if (sor != (1 << N) - 1) {  // 成立的话,说明存在某个字母不能出现在这一行
            memcpy(state, bstate[kcnt], sizeof state);
            memcpy(str, bstr[kcnt], sizeof str);
            return false;
        }
        for (int j = sand; j; j -= lowbit(j)) {  // 如果某个字母,只能填在一个位置上,则直接填
            int t = lowbit(j);
            if (!(drawn & t)) {  // 如果这个字母没有填过的话,才填写该字母
                for (int k = 0; k < N; k++)  // 判断应该将字母log[t]+'A'填写到哪个位置
                    if (state[i][k] & t) {
                        draw(i, k, log[t]);
                        cnt--;
                    }
            }
        }
    }

    // 对于每一列,如果某个字母不能出现在任何一个位置,则无解;
    // 如果某个字母,只能填在一个位置上,则直接填
    for (int i = 0; i < N; i++) {
        int sor = 0;  // sor中存储的是当前这一列可以备选方案的并集
        int sand = (1 << N) - 1;  // 某一位如果是1,表示这个字母只能在这一列出现一次
        int drawn = 0;  // 当前这一列已经填上的格子是哪些字母
        for (int j = 0; j < N; j++) {
            int s = state[j][i];
            sand &= ~(sor & s);;  // s & sor的结果中如果某一位是1,表示某个字母可以在该列中填写两次
            sor |= s;
            if (str[j][i] != '-') drawn |= state[j][i];
        }
        if (sor != (1 << N) - 1) {
            memcpy(state, bstate[kcnt], sizeof state);
            memcpy(str, bstr[kcnt], sizeof str);
            return false;
        }
        for (int j = sand; j; j -= lowbit(j)) {  // 如果某个字母,只能填在一个位置上,则直接填
            int t = lowbit(j);
            if (!(drawn & t)) {  // 如果这个字母没有填过的话,才填写该字母
                for (int k = 0; k < N; k++)  // 判断应该将字母log[t]+'A'填写到哪个位置
                    if (state[k][i] & t) {
                        draw(k, i, log[t]);
                        cnt--;
                    }
            }
        }
    }

    // 对于每个16宫格,如果某个字母不能出现在任何一个位置,则无解;
    // 如果某个字母,只能填在一个位置上,则直接填
    for (int i = 0; i < N; i++) {  // i表示16宫格的编号
        int sor = 0;  // sor中存储的是当前这个16宫格可以备选方案的并集
        int sand = (1 << N) - 1;  // 某一位如果是1,表示这个字母只能在这个16宫格出现一次
        int drawn = 0;  // 当前这个16宫格已经填上的格子是哪些字母
        for (int j = 0; j < N; j++) {  // y表示枚举在16宫格的位置
            int sx = i / 4 * 4, sy = i % 4 * 4;
            int dx = j / 4, dy = j % 4;
            int s = state[sx + dx][sy + dy];
            sand &= ~(sor & s);;  // s & sor的结果中如果某一位是1,表示某个字母可以在该行中填写两次
            sor |= s;
            if (str[sx + dx][sy + dy] != '-') drawn |= state[sx + dx][sy + dy];
        }
        if (sor != (1 << N) - 1) {
            memcpy(state, bstate[kcnt], sizeof state);
            memcpy(str, bstr[kcnt], sizeof str);
            return false;
        }
        for (int j = sand; j; j -= lowbit(j)) {  // 如果某个字母,只能填在一个位置上,则直接填
            int t = lowbit(j);
            if (!(drawn & t)) {  // 如果这个字母没有填过的话,才填写该字母
                for (int k = 0; k < N; k++) {  // 判断应该将字母log[t]+'A'填写到哪个位置
                    int sx = i / 4 * 4, sy = i % 4 * 4;
                    int dx = k / 4, dy = k % 4;
                    if (state[sx + dx][sy + dy] & t) {
                        draw(sx + dx, sy + dy, log[t]);
                        cnt--;
                    }
                }
            }
        }
    }

    if (!cnt) return true;

    // 每次选择方案时,选择备选方案最少的格子来填
    int x, y, s = 100;
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++)
            if (str[i][j] == '-' && ones[state[i][j]] < s) {
                s = ones[state[i][j]];
                x = i, y = j;
            }
    }

    memcpy(bstate2[kcnt], state, sizeof state);
    for (int i = state[x][y]; i; i -= lowbit(i)) {
        memcpy(state, bstate2[kcnt], sizeof state);
        draw(x, y, log[lowbit(i)]);
        if (dfs(cnt - 1)) return true;
    }

    memcpy(state, bstate[kcnt], sizeof state);
    memcpy(str, bstr[kcnt], sizeof str);

    return false;
}

int main() {

    // 初始化log数组,ones数组
    for (int i = 0; i < N; i++) log[1 << i] = i;
    for (int i = 0; i < 1 << N; i++) ones[i] = ones[i >> 1] + (i & 1);
    // 使用下面的方法求解ones也是可以的
    // for (int i = 0; i < 1 << N; i++) {
    //     for (int j = i; j; j -= lowbit(j)) ones[i]++;
    // }

    // 多组测试数据
    while (cin >> str[0]) {
        for (int i = 1; i < N; i++) cin >> str[i];

        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++)
                state[i][j] = (1 << N) - 1;
        }
        int cnt = 0;  // 存储需要填写的空格的个数
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++)
                if (str[i][j] != '-') draw(i, j, str[i][j] - 'A');
                else cnt++;
        }

        dfs(cnt);
        for (int i = 0; i < N; i++) cout << str[i] << endl;
        cout << endl;
    }

    return 0;
}
// 解法二:Dancing Links
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 20000;

int m = 16 * 16 * 4;  // 列数
int u[N], d[N], l[N], r[N], s[N], col[N], row[N], idx;

int ans[N], top;  // 存储选的行(行代表操作)

struct Op {
    int x, y;
    char z;
} op[N];  // 存储每行代表的操作

char g[N][N];  // 存储数独

void init() {
    
    for (int i = 0; i <= m; i++) {
        l[i] = i - 1, r[i] = i + 1;
        s[i] = 0;
        d[i] = u[i] = i;
    }
    l[0] = m, r[m] = 0;
    idx = m + 1;
}

void add(int &hh, int &tt, int x, int y) {
    
    row[idx] = x, col[idx] = y, s[y]++;
    u[idx] = y, d[idx] = d[y], u[d[y]] = idx, d[y] = idx;
    r[hh] = l[tt] = idx, r[idx] = tt, l[idx] = hh;
    tt = idx++;
}

void remove(int p) {
    r[l[p]] = r[p], l[r[p]] = l[p];
    for (int i = d[p]; i != p; i = d[i])
        for (int j = r[i]; j != i; j = r[j]) {
            s[col[j]]--;
            u[d[j]] = u[j], d[u[j]] = d[j];
        }
}

void resume(int p) {
    for (int i = u[p]; i != p; i = u[i])
        for (int j = l[i]; j != i; j = l[j]) {
            u[d[j]] = j, d[u[j]] = j;
            s[col[j]]++;
        }
    r[l[p]] = p, l[r[p]] = p;
}

bool dfs() {
    
    if (!r[0]) return true;
    
    int p = r[0];
    for (int i = r[0]; i; i = r[i]) 
        if (s[i] < s[p])
            p = i;
    if (!s[p]) return false;
    
    remove(p);
    for (int i = d[p]; i != p; i = d[i]) {
        ans[++top] = row[i];
        for (int j = r[i]; j != i; j = r[j]) remove(col[j]);
        if (dfs()) return true;
        for (int j = l[i]; j != i; j = l[j]) resume(col[j]);
        top--;
    }
    resume(p);
    return false;
}

int main() {
    
    while (~scanf("%s", g[0])) {
        
        for (int i = 1; i < 16; i++) scanf("%s", g[i]);
        
        init();  // 初始化DLX
        
        for (int i = 0, n = 1; i < 16; i++)  // n表示当前添加的行
            for (int j = 0; j < 16; j++) {
                int a = 0, b = 15;
                if (g[i][j] != '-') a = b = g[i][j] - 'A';
                for (int k = a; k <= b; k++, n++) {
                    int hh = idx, tt = idx;
                    op[n] = {i, j, k + 'A'};  // 记录操作
                    // (1) (i, j)这个格子只能填一个数
                    add(hh, tt, n, i * 16 + j + 1);
                    // (2) 每行中0~15恰好出现一次(第i行填写数字k)
                    add(hh, tt, n, 256 + i * 16 + k + 1);
                    // (3) 每列中0~15恰好出现一次(第j列填写数字k)
                    add(hh, tt, n, 256 * 2 + j * 16 + k + 1);
                    // (4) 每个方格0~15恰好出现一次(第i/4*4+j/4个方格填写数字k)
                    add(hh, tt, n, 256 * 3 + (i / 4 * 4 + j / 4) * 16 + k + 1);
                }
            }
        
        dfs();  // 求解答案
        
        for (int i = 1; i <= top; i++) {
            auto t = op[ans[i]];
            g[t.x][t.y] = t.z;
        }
        
        for (int i = 0; i < 16; i++) puts(g[i]);
        puts("");
    }
    
    return 0;
}
  • 7
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值