数独游戏,一个比较经典的搜索剪枝优化题,POJ中有2676,2918,3074,3076四道数独问题。其难度大致是2676=2918<3074<3076。下面这道用暴搜加上一个小剪枝就可以过。
https://cn.vjudge.net/contest/245662#problem/E
数独游戏的规则是这样的:在一个9x9的方格中,你需要把数字1-9填写到空格当中,并且使方格的每一行和每一列中都包含1-9这九个数字。同时还要保证,空格中用粗线划分成9个3x3的方格也同时包含1-9这九个数字。比如有这样一个题,大家可以仔细观察一下,在这里面每行、每列,以及每个3x3的方格都包含1-9这九个数字。
例题:
答案:
Input
本题包含多组测试,每组之间由一个空行隔开。每组测试会给你一个 9*9 的矩阵,同一行相邻的两个元素用一个空格分开。其中1-9代表该位置的已经填好的数,问号(?)表示需要你填的数。
Output
对于每组测试,请输出它的解,同一行相邻的两个数用一个空格分开。两组解之间要一个空行。
对于每组测试数据保证它有且只有一个解。
Sample Input
7 1 2 ? 6 ? 3 5 8
? 6 5 2 ? 7 1 ? 4
? ? 8 5 1 3 6 7 2
9 2 4 ? 5 6 ? 3 7
5 ? 6 ? ? ? 2 4 1
1 ? 3 7 2 ? 9 ? 5
? ? 1 9 7 5 4 8 6
6 ? 7 8 3 ? 5 1 9
8 5 9 ? 4 ? ? 2 3
Sample Output
7 1 2 4 6 9 3 5 8
3 6 5 2 8 7 1 9 4
4 9 8 5 1 3 6 7 2
9 2 4 1 5 6 8 3 7
5 7 6 3 9 8 2 4 1
1 8 3 7 2 4 9 6 5
2 3 1 9 7 5 4 8 6
6 4 7 8 3 2 5 1 9
8 5 9 6 4 1 7 2 3
思路:用a,b,c数组分别计录行,列 和 小矩阵中是否已经填入了某个数字,再用数组xx[]和yy[]记录空白位置的坐标。dfs的时候搜索这些坐标,依次查看从1到9的数字是否可以填入,如果可以填入则填入。一个数字如果可以填入某个位置,那它一定没有出现在这一行、这一列和这个小矩阵中。
#include<iostream>
using namespace std;
int v[20][20],xx[100],yy[100],num;
bool f,gg,a[20][20],b[20][20],c[20][20]; //a[i][x]记录第i行有没有用过x,b数组记录列,c数组记录小方块
void shuru(char s,int x,int y){
int q;
if (s=='?'){
num++;
xx[num]=x;
yy[num]=y;
v[x][y]=0;
}
else{
q=s-'0';
v[x][y]=q;
a[x][q]=1;
b[y][q]=1;
c[((x-1)/3)*3+((y-1)/3)+1][q]=1; //通过计算,用x和y表示出当前位置在第几个小方块。
}
}
void sousuo(int z) //z表示正在第几个空白位置。
{
int i,j,x,y,d;
if (z>num) {
gg=1;
return;
}
for (i=1;i<=9;i++)
{
x=xx[z];
y=yy[z];
d=((x-1)/3)*3+((y-1)/3)+1;
if (!a[x][i]&&!b[y][i]&&!c[d][i]){
a[x][i]=1;
b[y][i]=1;
c[d][i]=1;
v[x][y]=i;
sousuo(z+1);
if (gg) return;
a[x][i]=0;
b[y][i]=0;
c[d][i]=0;
}
}
}
int main(){
int i,j;
char s;
f=0;
while (cin>>s)
{
for (i=1;i<=9;i++)
for (j=1;j<=9;j++){
a[i][j]=0;
b[i][j]=0;
c[i][j]=0;
}
num=0;
shuru(s,1,1);
for (i=2;i<=9;i++) {
cin>>s;
shuru(s,1,i);
}
for (i=2;i<=9;i++)
for (j=1;j<=9;j++) {
cin>>s;
shuru(s,i,j);
}
gg=0;
sousuo(1);
if (f) cout<<endl;
for (i=1;i<=9;i++)
for (j=1;j<=9;j++){
cout<<v[i][j];
if (j==9) cout<<endl;
else cout<<' ';
}
f=1;
}
return 0;
}
下面的3074用刚才的方法就过不了了,需要加入其他的剪枝优化。首先,对于某个格子,如果某行或某列或某个小矩阵可以填入数字k的位置只有一个,那么就把数字k填入这个位置。然后,对于每一个空白位置统计它可以填入的数字个数,搜索的时候从可以填入的数字比较少的位置开始搜索。
快速失败的好处是我们避免了尝试后面的所有可能性。
在具体实现的时候,我们另开一个数组,来记录某一行或某一列或某一小矩阵的能填入数字k的位置个数。然后在搜索的时候,每次填入一个新的数字之后就扫描一遍所有的行,列和小矩阵,如果能填入数字k的位置只有一个,我们就将数字k填入这个位置。但是我们只记录了能填入数字k的位置个数,而没有记录它的位置,那么如何找到这个位置呢?我们枚举所有的空白位置,如果它是在这一个行/列/小矩阵,且可以填入这个数字,那么我们就将它填入数字k。这样,我们就将所有只能填入一个数字的位置都填好了。
那么现在有个问题,就是如何回溯。我们把所有能填入数字k的位置只有一个的情况都填上了,回溯的时候怎么把它撤回呢?我们应用一个小技巧,就是不真的把这个位置填上k,而是用bool型数组s记录下行/列/小矩阵能不能填数字k。如果是上述那种情况,我们就只把k赋值为真,其余都为假。然后这个位置能填的数字个数为1。(p[i]=1)
这样,在dfs的时候每次取p[i]最小的位置,再枚举数字k,如果s[i][k]为真就填入。
//s[i][j]记录的是第i个空白位置可不可以填数字j
#include <cstdio>
#include <string.h>
int v[20][20],xx[100],yy[100],p[100],num;
bool f,gg,a[20][20],b[20][20],c[20][20],s[100][10]; //a[i][x]记录第i行有没有用过x,b数组记录列,c数组记录小方块
void update() //更新
{
int oprow[10][10],opcol[10][10],opblock[10][10]; // 记录每行,列,小矩阵中数字k一共有几个可以放置的地方
memset(oprow,0,sizeof(oprow));
memset(opcol,0,sizeof(opcol));
memset(opblock,0,sizeof(opblock));
for (int i = 1; i <= num; i++)
{
int x=xx[i];
int y=yy[i];
if (v[x][y] == 0 ){
p[i]=0;
for (int j = 1; j <= 9 ; j++)
{
s[i][j]=0;
if (a[x][j] == 0 && b[y][j]==0 && c[(x-1)/3*3+(y-1)/3+1][j]==0){
p[i]++; //记录这个位置可以填入的数字个数
s[i][j]=1;
oprow[x][j]++;
opcol[y][j]++;
opblock[(x-1)/3*3+(y-1)/3+1][j]++;
}
}
}
}
for (int i = 1; i <= 9 ; i++) //第i行
for (int j = 1; j <= 9; j++) //填入数字j
{
if (oprow[i][j]==1)
{
for (int k = 1; k <= num; k++) //第k个空白位置
{
if (xx[k]==i && v[xx[k]][yy[k]]==0 && s[k][j]==1)
{
p[k]=1;
for (int u = 1; u <=9 ; u++)
{
s[k][u]=0;
}
s[k][j]=1;
break;
}
}
}
if (opcol[i][j]==1)
{
for (int k = 1; k <= num; k++)
{
if (yy[k]==i && v[xx[k]][yy[k]]==0 && s[k][j]==1)
{
p[k]=1;
for (int u = 1; u <=9 ; u++)
{
s[k][u]=0;
}
s[k][j]=1;
break;
}
}
}
if (opblock[i][j]==1)
{
for (int k = 1; k <= num; k++)
{
if ((xx[k]-1)/3*3+(yy[k]-1)/3+1==i && v[xx[k]][yy[k]]==0 && s[k][j]==1)
{
p[k]=1;
for (int u = 1; u <=9 ; u++)
{
s[k][u]=0;
}
s[k][j]=1;
break;
}
}
}
}
}
int getnow() //找到当前可填入数字最少的位置
{
int tmp=10, mark=-1, x, y;
for (int i=1; i<=num; i++){
x=xx[i];
y=yy[i];
if (tmp > p[i] && v[x][y] == 0){
tmp = p[i];
mark = i;
}
}
return mark;
}
void shuru(char s,int x,int y){
int q;
if (s=='.'){
num++;
xx[num]=x;
yy[num]=y;
v[x][y]=0;
}
else{
q=s-'0';
v[x][y]=q;
a[x][q]=1;
b[y][q]=1;
c[((x-1)/3)*3+((y-1)/3)+1][q]=1; //通过计算,用x和y表示出当前位置在第几个小方块。
}
}
void sousuo(int z) //z表示正在第几个空白位置。
{
int i,x,y,d;
if (z>num) {
gg=1;
return;
}//printf("%d\n",z );
int now=getnow(); //当前可填入数字最少的位置
if (now==-1) return;
x=xx[now];
y=yy[now];
for (i=1;i<=9;i++)
if (s[now][i])
{
d=((x-1)/3)*3+((y-1)/3)+1;
a[x][i]=1;
b[y][i]=1;
c[d][i]=1;
v[x][y]=i;
update();
sousuo(z+1);
if (gg) return;
a[x][i]=0;
b[y][i]=0;
c[d][i]=0;
v[x][y]=0;
update();
}
}
void init()
{
int i,j;
char s[100];
f=0;
while (gets(s))
{
if (s[0]=='e') break;
num=0;
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
memset(c,0,sizeof(c));
for (i=1;i<=9;i++)
for (j=1;j<=9;j++) {
int x=(i-1)*9+j-1;
shuru(s[x],i,j);
}
gg=0;
update();
sousuo(1);
if (f) printf("\n");
for (i=1;i<=9;i++)
for (j=1;j<=9;j++){
printf("%d",v[i][j]);
}
f=1;
}
}
int main(){
init();
return 0;
}