AcWing 166 数独

题目描述:

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

请编写一个程序填写数独。

输入格式

输入包含多组测试用例。

每个测试用例占一行,包含81个字符,代表数独的81个格内数据(顺序总体由上到下,同行由左到右)。

每个字符都是一个数字(1-9)或一个”.”(表示尚未填充)。

您可以假设输入中的每个谜题都只有一个解决方案。

文件结尾处为包含单词“end”的单行,表示输入结束。

输出格式

每个测试用例,输出一行数据,代表填充完全后的数独。

输入样例:

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

首先介绍普通的暴搜,就是自左而右,自上而下的去填充数字,直到找到最优解为止。状态表示可以用状态压缩实现,要想在某个位置上填充x,就需要判断x是否在同一行、同一列以及同一个九宫格出现过。以r[i]表示第i行的状态为例,r[i] = 100010001表示第i行1,5,9已经被填充了,我们如果想在第i行的某个空位上填充k,就需要先判断r[i] >> k & 1是否为0,只有r[i]的第k位是0才可以填充,当然,还需要用c[i]表示第i列的状态。一共有9个九宫格,也用0-8去编号下,再用w[i]表示第i个九宫格的状态。只要k在同一行、同一列、同一个九宫格都没有出现过,就可以去填充了。这里实现的细节要注意,为了方便,读入时比如第0行填充了3,我们需要写成r[0] += 1 << 3;就算1-9全部填满,也只是把二进制的1-9位全部置为了1,而第0位我们不用去管它,也就是说,实际上我们是维护一个宽度为10的二进制数,然后只去考虑第1-9位上的数是否为1。

dfs的过程为:找到一个空位,枚举1-9,当这个数不曾出现在同一行、同一列、同一九宫格的时候就填充这个数,并且把该行、该列、该九宫格这个数对应的二进制位置1,然后遍历下一个位置,遍历完后也要恢复所有全局数组的状态。

另外,由于每个数独的答案可能不唯一,找到其中一个就可以返回,不用继续dfs了。如果只有单个输入,找到解后直接exit(0)即可,但是本题是多组用例,所以为了让dfs尽快返回,让递归栈里的内容快速弹出,这里在找到最优解后设置一个标志变量flag的值为true,后面dfs只要遇见flag是true的情况直接返回即可。具体实现细节见代码:

#include <iostream>
#include <string>
#include <cstring>
using namespace std;
const int N = 10;
int g[N][N],r[N],c[N],w[N];
bool flag;
int get(int x,int y){//返回九宫格的编号
    x /= 3,y /= 3;
    return x * 3 + y;
}
void dfs(int x,int y,int s){
    if(flag)    return;
    if(!s){//没有空格子了
        for(int i = 0;i < 9;i++){
            for(int j = 0;j < 9;j++){
                cout<<g[i][j];
            }
        }
        cout<<endl;
        flag = true;
        return;
    }
    int nx = x,ny = y + 1;//下一个位置
    if(ny == 9)    nx++,ny = 0;//越界了就从下一行开始
    if(g[x][y] == 0){
        int u = get(x,y);
        for(int i = 1;i <= 9;i++){
            if(!(r[x]>>i & 1) && !(c[y]>>i & 1) && !(w[u]>>i & 1)){
                r[x] += 1 << i;
                c[y] += 1 << i;
                w[u] += 1 << i;
                g[x][y] = i;
                dfs(nx,ny,s - 1);
                r[x] -= 1 << i;//恢复状态
                c[y] -= 1 << i;
                w[u] -= 1 << i;
                g[x][y] = 0;
            }
        }
    }
    else    dfs(nx,ny,s);
}
int main(){
    string str;
    while(cin>>str && str[0] != 'e'){
        memset(g,0,sizeof g);
        memset(r,0,sizeof r);
        memset(c,0,sizeof c);
        memset(w,0,sizeof w);
        flag = false;
        int s = 0;
        for(int i = 0;i < str.size();i++){
            if(str[i] == '.'){
                s++;
                continue;
            }
            int x = i / 9,y = i % 9;
            g[x][y] = str[i] - '0';
            r[x] += 1 << g[x][y];
            c[y] += 1 << g[x][y];
            int u = get(x,y);
            w[u] += 1 << g[x][y];
        }
        dfs(0,0,s);
    }
    return 0;
}

方法二:DFS + 优化搜索顺序剪枝

上面的代码虽然思路比较简洁,按顺序去dfs,在本题中却会超时,当然,如果不是多组测试用例肯定不会超时的,因此,需要进行剪枝。首先要注意到按顺序dfs可能会造成很大的冗余。比如说一个数独初始时最后一行除了第一列都已经有数字了,只剩下2可以被填充到第一个位置了。如果是我们人去写数独,肯定是先写这种可以填充的数是唯一的位置,然后再去写其他位置。为什么我们会采取这样的策略呢?一开始我们就知道最后一行第一列只能填充2,所以其他行的第一列都不能填2,否则就找不到解了。但是方法一的这种搜索必然会导致,枚举第一行第一列时填个2,此时没有违背数独的规则,然后沿着搜索树中2后面的分支搜索个遍,没找到答案再去改第一行第一列上的数。不止如此,dfs在枚举第二行、第三行一直到第八行第一列位置上的数时也都会考虑2,这将耗费大量的时间。如果我们最先枚举的就是最后一行第一列,先把2填进去,这些冗余都是可以避免的,所以,本题搜索顺序很重要。我们应该优先搜索能填数字少的地方。

方法一中,在枚举(x,y)上能填充的数字时,我们是先看这个数字是否在第x行、第y列、第u个九宫格中出现过,设此时行、列、九宫格的状态分别是r、c和w,也就是说。我们是判断r中第k为是0,c中第k位是0,w中第k为也是0才去填充k的。设st = r | c | w,三个对应位都是0的位置才能够填充,因此只要三种状态或起来的状态st的第k为是0就可以填充了,st状态中有几个0,就代表这个位置可以填几种数,我们需要优先枚举0最少也就是1最多的状态。换而言之,就是要遍历数独中所有的空位,找到这些位置中st状态中1最多的位置优先填充。一个二进制数中有多少个1,可以先预处理出来。前面说过,这里状态表示实际上是用了10位的二进制数,所以将2^10 = 1024种状态中1的个数都记录下来即可。统计1的个数用lowbit运算即可,比较简单这里不再赘述了。

#include <iostream>
#include <string>
#include <cstring>
using namespace std;
const int N = 10,M = 1 << 10 + 1;
int g[N][N],r[N],c[N],w[N],one[M];
bool flag;
int get(int x,int y){
    x /= 3,y /= 3;
    return x * 3 + y;
}
void dfs(int s){
    if(flag)    return;
    if(!s){
        for(int i = 0;i < 9;i++){
            for(int j = 0;j < 9;j++){
                cout<<g[i][j];
            }
        }
        cout<<endl;
        flag = true;
        return;
    }
    int st,x,y,res = -1;
    for(int i = 0;i < 9;i++){
        for(int j = 0;j < 9;j++){
            if(g[i][j])    continue;
            int u = get(i,j);
            int t = r[i] | c[j] | w[u];
            if(one[t] > res){//找到1最多的位置
                st = t;
                res = one[t];
                x = i,y = j;
            }
        }
    }
    int u = get(x,y);
    for(int i = 1;i <= 9;i++){
        if(!(st >> i & 1)){
            r[x] += 1 << i;
                c[y] += 1 << i;
                w[u] += 1 << i;
                g[x][y] = i;
                dfs(s - 1);
                r[x] -= 1 << i;
                c[y] -= 1 << i;
                w[u] -= 1 << i;
                g[x][y] = 0;
        }
    }
}
int main(){
    string str;
    for(int i = 0;i < 1 << 10;i++){//预处理1的个数
        int j = i;
        while(j){
            j -= (-j) & j;
            one[i]++;
        }
    }
    while(cin>>str && str[0] != 'e'){
        memset(g,0,sizeof g);
        memset(r,0,sizeof r);
        memset(c,0,sizeof c);
        memset(w,0,sizeof w);
        flag = false;
        int s = 0;
        for(int i = 0;i < str.size();i++){
            if(str[i] == '.'){
                s++;
                continue;
            }
            int x = i / 9,y = i % 9;
            g[x][y] = str[i] - '0';
            r[x] += 1 << g[x][y];
            c[y] += 1 << g[x][y];
            int u = get(x,y);
            w[u] += 1 << g[x][y];
        }
        dfs(s);
    }
    return 0;
}

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值