[POJ3076] Sudoku

参考:https://www.luogu.com.cn/problem/solution/SP1110

https://blog.csdn.net/a_bright_ch/article/details/81950224

A 题目

   B 解题

想想我们 9*9 的数独是怎么做的?

我们加了一个搜索顺序优化剪枝:“优先选择能填的数字最少的位置“,并且只有当某个位置无法填数时才判定失败。

但是这个剪枝对于 4*4 肯定是不够的,观察局部情况:(图源

如上图,如图,虽然每个位置都有能填的数,但是由于下面两个B的影响,导致B不可能填入第一行的任何一个空位。类似的还有别的更复杂的情况。

也就是说,我们需要对数独进行更加全面的可行性判定,尽早发现无解的分支并回溯

我们可以加入以下的可行性剪枝:

1.遍历当前所有空格

(1)如果某个空格不能填任何数,即判定分支失败,立即回溯;

(2)如果某个空格只能填一个数,立即填写;

2.考虑所有的行/列/十六宫格:

(1)如果某个字母无法填在该行/列/十六宫格的任何空位,立即回溯;

(2)如果某个字母只能填在该行/列/十六宫格的某个空位,立即填写.

3.选择可填字母最少的位置,枚举填写那个字母作为分支

C 代码

//充足注释

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef unsigned short us;//16位二进制优化常数 
const int N = 20;

int mp[N][N],cnt;
us vis[N][N];//vis[i][j]表示(i,j)可以填的数有哪些,0可填,1不可填
char ar[N];

void init(){
    cnt=0;
    memset(mp,0,sizeof(mp));
    memset(vis,0,sizeof(vis));
}
void change(int x,int y,int ch){//(x,y)填a
    cnt++;//记录填了多少个数 
    mp[x][y]=ch;
    for(int i=0;i<16;i++){   //n|(1<<k):把n在二进制表示下的k位赋值1 
        vis[x][i]|=1<<(ch-1);//标记行 
        vis[i][y]|=1<<(ch-1);//标记列 
    }
    int gx=x/4*4,gy=y/4*4;//计算宫格左上角坐标 
    for(int i=gx;i<=gx+3;i++)
        for(int j=gy;j<=gy+3;j++)
            vis[i][j]|=1<<(ch-1);
}
int get_one(us x){//得到x二进制下第一个1 
    for(int i=0;x;i++){
        if(x&1){
            if(x>>1==0)return i;
            return -1;//多个1则不满足剪枝条件 
        }
        x>>=1;
    }
    return -1;
}
int hang(int x,int k){
    /*第x行,数字k+1。返回>0表示唯一可填k+1的位置;
    返回-1表示出现超过1次;返回-2表示k+1没有可以填的位置 */ 
    int p=-1;
    for(int i=0;i<16;i++){
        if(mp[x][i]==k+1)return -1;//k+1已填 
            //注意这句话必须在continue之前,不然可能会return -2 
        if(mp[x][i]>0)continue;
        if((vis[x][i]&1<<k)==0){//n&(1<<k)取出n在二进制下第k位 
            if(p!=-1)return -1;//出现过,不满足剪枝要求 
            p=i;
        }
    }
    if(p!=-1)return p;
    return -2;
}
int lie(int y,int k){//第y行,数字k+1 
    int p=-1;
    for(int i=0;i<16;i++){
        if(mp[i][y]==k+1)return -1;
        if(mp[i][y]>0)continue;
        if((vis[i][y]&1<<k)==0){
            if(p!=-1)return -1;
            p=i;
        }
    }
    if(p!=-1)return p;
    return -2;
}
void gong(int gx,int gy,int k,int &x,int &y){
    //以(gx,gy)为左上角的宫格,数字k+1,(x,y)为唯一可填坐标 
    //注意这里 x,y 加上了引用以避免return时make_pair的麻烦 
    x=-2;
    for(int i=gx;i<=gx+3;i++){
        for(int j=gy;j<=gy+3;j++){
            if(mp[i][j]==k+1)return x=-1,void();//等价于if(...){x=-1,return;} 
            if(mp[i][j]>0)continue;
            if((vis[i][j]&1<<k)==0){
                if(x!=-2)return x=-1,void();
                x=i;y=j;
            }
        }
    }
}
int count_one(us x){//求x中1的个数 
    int cnt=0;
    while(x){
        if(x&1)cnt++;
        x>>=1;
    }
    return cnt;
}
void print(){
    for(int i=0;i<16;i++){
        for(int j=0;j<16;j++)
            printf("%c",mp[i][j]+'A'-1); 
        printf("\n");
    }
    puts("");
} 

bool dfs(){
    if(cnt==256)return true;//填满了 
    //1.遍历当前所有空格
    for(int i=0;i<16;i++){
        for(int j=0;j<16;j++){
            if(mp[i][j]>0)continue;
            int k=get_one(vis[i][j]);
            if(k!=-1)change(i,j,k+1);
        }
    }
    //2.考虑所有的行/列/十六宫格
    for(int i=0;i<16;i++){
        for(int j=0;j<16;j++){
            int y=hang(i,j);
            if(y==-2)return false;
            if(y!=-1)change(i,y,j+1);
        }
    }
    for(int i=0;i<16;i++){
        for(int j=0;j<16;j++){
            int x=lie(i,j);
            if(x==-2)return false;
            if(x!=-1)change(x,i,j+1);
        }
    }
    for(int i=0;i<16;i+=4){
        for(int j=0;j<16;j+=4){
            for(int k=0;k<16;k++){
                int x,y;
                gong(i,j,k,x,y);
                if(x==-2)return false;
                if(x!=-1)change(x,y,k+1);
            }
        }
    }
    if(cnt==256)return true;
    //3.选择可填字母最少的位置,枚举填写那个字母作为分支
    int t_cnt=cnt,t_mp[N][N];
    us t_table[N][N];//为回溯做备份 
    int mx=-1,mxx,mxy;
    for(int i=0;i<16;i++){
        for(int j=0;j<16;j++){
            t_mp[i][j]=mp[i][j];
            t_table[i][j]=vis[i][j];
            if(mp[i][j]>0)continue;
            int temp=count_one(vis[i][j]);
            if(temp>mx)mx=temp,mxx=i,mxy=j;
        } 
    }
    for(int i=0;i<16;i++){
        if((vis[mxx][mxy]&1<<i)==0){
            change(mxx,mxy,i+1);//注意+1 
            if(dfs())return true;
            cnt=t_cnt;//回溯 
            for(int j=0;j<16;j++)
                for(int k=0;k<16;k++)
                    mp[j][k]=t_mp[j][k],vis[j][k]=t_table[j][k];
        }
    }
    return false;
}

int main(){
    while(1){
        init();
        for(int i=0;i<16;i++){
            if(scanf("%s",ar)==EOF)return 0;
            for(int j=0;j<16;j++){
                if(ar[j]!='-')change(i,j,ar[j]-'A'+1);
            }
        }
        dfs();
        print();
    }
    return 0;
}
View Code

D 多解

机房某大佬说可以用DLX???

太菜了,不会,找时间补

 

posted @ 2021-02-06 00:35  _Famiglistimo  阅读(106)  评论(0编辑  收藏  举报