通俗易懂的方法就是dfs簡單爆搜,本文介紹的是最簡單粗暴的方法,直接暴力搜索,沒有進行任何優化。
數獨又稱爲9宮格(如下圖所示)
每一行的數字都不能重複
每一列的數字都不能重複
每一個小9宮格(方陣)數字都不能重複
可以看做上面三個分別是一個包含1-9的集合
因此我們可以設置三個二維數組來記錄每一行、每一列、每一個方陣1-9的數字,從而確定每個點可以填哪些數。一維分別表示第幾行、第幾列、第幾個方陣,然後二維用來記錄哪些數字已經存在,1表示存在,0表示不存在。
假定我們輸入的數據如下:
8 0 0 0 0 0 0 0 0
0 0 3 6 0 0 0 0 0
0 7 0 0 9 0 2 0 0
0 5 0 0 0 7 0 0 0
0 0 0 0 4 5 7 0 0
0 0 0 1 0 0 0 3 0
0 0 1 0 0 0 0 6 8
0 0 8 5 0 0 0 1 0
0 9 0 0 0 0 4 0 0
那麼,我們設置一個二維數組a[10][10]來進行圖的存儲,然後再對每個輸入點進行判斷,當前輸入的點對應的值如果不爲0,那麼就對這個點進行記錄。row[10][10]記錄行、col[10][10]記錄列、g[10][10]記錄方陣。以輸入的第二行第三列的點(值爲3)爲例(我們數組下標從1開始,爲了方便)
a[2][3]>0,那麼進行點的標記
row[2][a[i][j]]=1、col[3]a[i][j]]=1、g[((i-1)/3)*3+(j-1)/3+1)][a[i][j]]=1。
上面的三個標記也就是數獨的精華了,把這裏理解了,數獨這個題目也就很容易理解了。
這裏最難的地方就是方陣的邊界問題,一不小心就可能錯了。因爲我們要將1-9劃分成3個部分,但是1~9除以3的話邊界不好處理3/3等於1,但是我們想讓他分在第一塊,也就是它應該等於0,6/2的話應該讓他等於1,應該和4/3、5/3都等於1,因爲他們在同一個方陣,那麼我們就得把1-9映射到0-8然後再進行劃分
(0~2)/3對應的是0
(3~5)/3對應的是1
(6~8)/3對應的是2
這樣就完美的避免了這個問題,我們設置的下標是從1開始的,但是這樣得到的結果是從0開始,因此我們給結果加上1,就從0-8變成了1-9。也就是9個方陣的編號。
到這裏我們就可以處理每個點可以選擇哪些數據,然後dfs遍歷找到合適的情況。
那麼還有一個問題就是從哪個點開始找,然後怎麼找!!!
題目都說了暴力搜索嘛,那麼我就從位置(1,1)開始搜索,一列一列的搜索,搜索完一行,那麼從下一行第一個數據開始。題目肯定保證數獨有解,那麼當搜到位置(9,9)時,那麼也就到了終點,就說明完成了數獨的填充,因此,直接打印填充好的圖就可以了。(理解的同學可以自己先寫寫代碼,不理解的童鞋可以藉助下面的代碼理解理解)
代碼在AcWing和落谷均已通過測試
AcWing上的題目鏈接:簡單數獨
#include<iostream>
#include<cstdio>
using namespace std;
const int N=10;
//a用來出存圖、col[i]記錄第i列已經存在的數
//row[i]記錄第i行已經存在的數、g[i]記錄第i個塊已經存在的數
int a[N][N],col[N][N],row[N][N],g[N][N];
//inline 是內聯函數,非遞歸函數使用纔有作用
//作用就是程序不把他當做函數,節省了程序調用函數的時間
//使用inline的目的就是爲了節省時間,快!!!
inline void pr()
{
for(int i=1;i<N;i++)
{
for(int j=1;j<N;j++)
printf("%d",a[i][j]);
puts("");//這麼寫相當於空格
}
exit(0);//這裏還是稍微優化了一下,找到就直接結束程序,沒必要再繼續找
}
void dfs(int x,int y)
{
//如果當前節點有值就不需要填充,直接跳到下一個節點
if(a[x][y]!=0)
if(x==N-1&&y==N-1) pr();//全部填充完畢
else if(y==N-1) dfs(x+1,1);//x行已經填充完畢
else dfs(x,y+1);//x行一列一列填充
else
for(int i=1;i<N;i++)
{
//判斷當前節點已經存在值i,不存在執行填充
if(!row[x][i]&&!col[y][i]&&!g[((x-1)/3)*3+(y-1)/3+1][i])
{
//填充操作,對點的狀態進行標註
a[x][y]=i,row[x][i]=1,col[y][i]=1,g[((x-1)/3)*3+(y-1)/3+1][i]=1;
if(x==N-1&&y==N-1) pr();//點填充完恰好是圖填充完
else if(y==N-1) dfs(x+1,1);//填充完對下一個點進行操作
else dfs(x,y+1);
//回溯,狀態還原
a[x][y]=0,row[x][i]=0,col[y][i]=0,g[((x-1)/3)*3+(y-1)/3+1][i]=0;
}
}
}
int main()
{
char temp;
for(int i=1;i<N;i++)
for(int j=1;j<N;j++)
{
cin>>temp;
//對輸入的char數據進行存放、1-9對應爲1-9,'.'對應爲0
a[i][j]=temp>='0'&&temp<='9'?int(temp-'0'):0;
//標註不爲0的點情況
if(a[i][j]) g[((i-1)/3)*3+(j-1)/3+1][a[i][j]]=1,row[i][a[i][j]]=1,col[j][a[i][j]]=1;
}
//從1,1開始順序搜索,一行一行從左到右進行搜索
dfs(1,1);
return 0;
}
當然,這麼暴力的話,對於難一點的數據,肯定是會TLE的,那麼我們就需要對代碼進行優化,分爲以下幾個方面:
1、選擇已填充個數多(行、列和方陣都滿足)的位置作爲填充的起點
2、適當剪枝,對沒有必要繼續執行的分支,不再執行
3、使用位運算提高運算速度
上圖是簡單數獨這個題目oj上運行時間,優化之後可以說的是快的一批呀!!!優化之後的代碼可以提交這個題目:進階版數獨,數據比較難(優化之後的代碼在進階版數獨都跑了600+ms),有興趣的可以試一下,這個的題目我已通過,之後我會發布詳細題解,先附上AC代碼,有興趣的童鞋可以先看看。
#include<iostream>
#include<algorithm>
using namespace std;
const int N=9;
char str[100];
int row[N],col[N],chunk[3][3],tab[1<<N],map[1<<N];
inline int lowbit(int x)
{
return x&-x;
}
inline 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++)
chunk[i][j]=(1<<N)-1;
}
inline int get(int x,int y)
{
return row[x]&col[y]&chunk[x/3][y/3];
}
bool dfs(int cnt)
{
if(!cnt) return true;
int x,y,mmin=10,t;
for(int i=0;i<N;i++)
for(int j=0;j<N;j++)
if(str[i*9+j]=='.')
{
t=tab[get(i,j)];
if(t<mmin) mmin=t,x=i,y=j;
}
for(int i=get(x,y);i;i-=lowbit(i))
{
t=map[lowbit(i)];
row[x]-=1<<t;
col[y]-=1<<t;
chunk[x/3][y/3]-=1<<t;
str[x*9+y]='1'+t;
if(dfs(cnt-1)) return true;
row[x]+=1<<t;
col[y]+=1<<t;
chunk[x/3][y/3]+=1<<t;
str[x*9+y]='.';
}
return false;
}
int main()
{
for(int i=0;i<N;i++) map[1<<i]=i;
for(int i=0;i<1<<N;i++)
{
int s=0;
for(int j=i;j;j-=lowbit(j)) s++;
tab[i]=s;
}
for(int i=0;i<N*N;i++) cin>>str[i];
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';
row[i]-=1<<t;
col[j]-=1<<t;
chunk[i/3][j/3]-=1<<t;
}
else cnt++;
dfs(cnt);
for(int i=0;i<N;i++)
{
for(int j=0;j<N;j++)
printf("%c",str[i*N+j]);
puts("");
}
return 0;
}