[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; }
D 多解
机房某大佬说可以用DLX???
太菜了,不会,找时间补