1.數獨題目傳送門:https://www.acwing.com/problem/content/168/
2.靶形數獨題目傳送門:https://www.acwing.com/problem/content/185/
題目1是一個普通的數獨,並且測試數據保證有解,但是測試數據是多組,在搜索上不講技巧的搜索是會TLE的,就連bitset去處理狀態都會。
題目2是在一個普通的數獨的基礎上加了兩點不同,第一不同是數據裏有沒有解的情況,第二不同是在九宮格上還有權值,要在所有方案中尋找最優值,即找到一種方案還不行,需要在填滿後,按照規則每個格子需要乘上權值後求和,輸出最大的和值。那麼需要把所有方案都找出並維護最大值。,還好這題數據裏只有一個需要解決的數獨問題。
那麼把題目1的數據求解後,再做第二題就顯得比較容易了。
那麼對於一個普通的數獨問題,我們採用如何的策略去填數獨呢?
1.找橫、豎和小九宮格中空白處能填的“合法數字”個數最少的格子去填,我想你如果手動填數獨也會是這個策略。如果有最少可以選擇的數的空白格里可以填的數有多個,暴力枚舉這幾個數搜索回溯,而不是隨意找個空白格去填。
2.如何快速確定每個位置上能填的合法數字呢,等到要找時,再去一遍掃行,一遍掃列,一遍掃九宮格尋找,顯然速度就會慢了些。
在此我們可以運用位運算來進行統計。具體方法是:
- 對於每行、每列、每個九宮格的3個二進制數(全局整數變量)保存哪些數字可以填。
- 對於每個位置,把它所在行、所在列和所在小九宮格的3個二進制數進行位與運算,就可以看到哪些數字是可以填的。如,假設(1,1)這個位置的行上的狀態是100111000,列上的狀態是011011011,小九宮格上的狀態是110111100,那麼這個位上可以用的數有2個,分別是4,5.因爲位與運算後從右往左數第4個位上和第5個位上是1,就表示這兩個數沒有出現過。程序實現時可以參考lowbit,取最低位的1採用(x & -x)的方式。
100111000
011011011
&110111100
--------
000011000 - 當一個位置上嘗試放入一個可以合法的數後,把該位置的行列和小九宮格的狀態該位置上的數置爲0,回溯時改回1即可還原現場。
題目1具體代碼是:
//同樣的思路用bitset去處理狀態超時,用位運算就AC了,可見bitiset在處理二進制數做
//狀態時在速度上稍顯不足。
#include<bits/stdc++.h>
using namespace std;
int sd[10][10],cnt =0;
int row[9],col[9],rcsmall[9];
int num[513],onenum[513];
char c;
int rcno[10][10]={{0,0,0,0,0,0,0,0,0,0},
{0,1,1,1,2,2,2,3,3,3},
{0,1,1,1,2,2,2,3,3,3},
{0,1,1,1,2,2,2,3,3,3},
{0,4,4,4,5,5,5,6,6,6},
{0,4,4,4,5,5,5,6,6,6},
{0,4,4,4,5,5,5,6,6,6},
{0,7,7,7,8,8,8,9,9,9},
{0,7,7,7,8,8,8,9,9,9},
{0,7,7,7,8,8,8,9,9,9},
};
struct pos{
int x,y;
};
void print(){
for(int i = 1;i<= 9 ;i++){
for(int j =1; j<= 9; j++){
printf("%d",sd[i][j]);
}
}
printf("\n");
}
pos find(){
int minn =10;
pos temp;
temp.x = -1,temp.y = -1;
for(int i =1;i<= 9;i++){
for(int j = 1;j<= 9; j++){
if(sd[i][j]==0){
int no = rcno[i][j];
int vs = row[i] & col[j] & rcsmall[no];
if(minn >onenum[vs]){
minn = onenum[vs];
temp.x = i;
temp.y = j;
}
}
}
}
return temp;
}
bool dfs(int k){
if(k == cnt+1){
print();return true;
}
pos p = find(); //找空白位置上可以放置的數據選擇性最小的哪個座標點。
int no = rcno[p.x][p.y];
int x = p.x,y = p.y;
int vs = row[x] & col[y] & rcsmall[no];//vs存儲的是二進制位上是1的對應數可選。
for( ; vs ; vs = vs - (vs & -vs)){//通過尋找最低位爲1的位置,逐漸枚舉每個可以放入的數。
int t =num[vs & -vs];
row[x] ^= 1<< (t-1);
col[y] ^= 1<< (t-1);
rcsmall[no] ^= 1<< (t-1);
sd[x][y] = t;
if(dfs(k+1)) return true;
sd[x][y] = 0;
row[x] ^= 1<< (t-1);
col[y] ^= 1<< (t-1);
rcsmall[no] ^= 1<< (t-1);
}
return false;
}
void init(){
for(int i = 0;i<10;i++){
row[i] = (1<<9) - 1;
col[i]= (1<<9) - 1;
rcsmall[i]= (1<<9) - 1;
}
}
void read(){
c = getchar();
if(c == 'e') return;
sd[1][1] = c =='.' ? 0: c-48;
for(int i = 2;i<= 81; i++){
c = getchar();
sd[(i-1)/9 +1][(i-1) % 9+1] = c =='.' ? 0: c-48;
}
c = getchar(); //?üê???DD?£
}
void pre(){
//預處理原始表中每行每列每個九宮格中數字的使用狀態。
for(int i = 1;i<= 9 ;i++){
for(int j =1; j<= 9; j++){
int no = rcno[i][j];
if(sd[i][j] != 0){
row[i] ^= 1<< (sd[i][j]-1);
col[j] ^= 1<< (sd[i][j]-1);
rcsmall[no] ^= 1<< (sd[i][j]-1);
}
else{
cnt ++;
}
}
}
}
int main(){
//預處理每個數上對應的二進制數中有幾個1,用於查找最少合法數據的位置時用。
for(int i =0; i< (1 << 9);i++)
for(int j = i; j; j = j - (j & -j))
onenum[i] ++;
//預處理數的二進制數中只有一個1時,它代表的時哪個數。
for(int i = 1;i<= 9 ;i ++){
num[1<< (i-1)] = i;
}
while(1){
cnt = 0;
init();
read();
if(c == 'e')break;
pre();
dfs(1);
}
return 0;
}
題目二的題解相比題目1,需要做以下更改:
- 輸入方式不一樣。
- 增加一個權值數組。
- 對出現填滿時由輸出改爲對數值的計算並維護。
- 出現填滿的方案時,需要繼續尋找下面的其他方案,因此去掉函數中的返回true,false之類的。
- 在出現方案時立個flag,帶搜索完畢檢查其flag的狀態來確定是否有解。
具體代碼如下:
#include<bits/stdc++.h>
using namespace std;
int sd[10][10],cnt =0;
int row[9],col[9],rcsmall[9];
int num[513],onenum[513];
int ans = 0;
char c;
bool flag = false;
int rcno[10][10]={{0,0,0,0,0,0,0,0,0,0},
{0,1,1,1,2,2,2,3,3,3},
{0,1,1,1,2,2,2,3,3,3},
{0,1,1,1,2,2,2,3,3,3},
{0,4,4,4,5,5,5,6,6,6},
{0,4,4,4,5,5,5,6,6,6},
{0,4,4,4,5,5,5,6,6,6},
{0,7,7,7,8,8,8,9,9,9},
{0,7,7,7,8,8,8,9,9,9},
{0,7,7,7,8,8,8,9,9,9},
};
int score[10][10]={
{0,0,0,0,0,0,0,0,0,0},
{0,6,6,6,6,6,6,6,6,6},
{0,6,7,7,7,7,7,7,7,6},
{0,6,7,8,8,8,8,8,7,6},
{0,6,7,8,9,9,9,8,7,6},
{0,6,7,8,9,10,9,8,7,6},
{0,6,7,8,9,9,9,8,7,6},
{0,6,7,8,8,8,8,8,7,6},
{0,6,7,7,7,7,7,7,7,6},
{0,6,6,6,6,6,6,6,6,6},
};
struct pos{
int x,y;
};
void getPerfect(){
int sum = 0;
for(int i = 1;i<= 9 ;i++){
for(int j =1; j<= 9; j++){
sum += sd[i][j]*score[i][j];
}
}
ans = max(ans,sum);
}
pos find(){
int minn =10;
pos temp;
temp.x = -1,temp.y = -1;
for(int i =1;i<= 9;i++){
for(int j = 1;j<= 9; j++){
if(sd[i][j]==0){
int no = rcno[i][j];
int vs = row[i] & col[j] & rcsmall[no];
if(minn >onenum[vs]){
minn = onenum[vs];
temp.x = i;
temp.y = j;
}
}
}
}
return temp;
}
void dfs(int k){
if(k == cnt+1){
flag = true;
getPerfect();
return;
}
pos p = find(); //找空白位置上可以放置的數據選擇性最小的哪個座標點。
int no = rcno[p.x][p.y];
int x = p.x,y = p.y;
int vs = row[x] & col[y] & rcsmall[no];//vs存儲的是二進制位上是1的對應數可選。
for( ; vs ; vs = vs - (vs & -vs)){//通過尋找最低位爲1的位置,逐漸枚舉每個可以放入的數。
int t =num[vs & -vs];
row[x] ^= 1<< (t-1);
col[y] ^= 1<< (t-1);
rcsmall[no] ^= 1<< (t-1);
sd[x][y] = t;
dfs(k+1);
sd[x][y] = 0;
row[x] ^= 1<< (t-1);
col[y] ^= 1<< (t-1);
rcsmall[no] ^= 1<< (t-1);
}
}
void init(){
for(int i = 0;i<10;i++){
row[i] = (1<<9) - 1;
col[i]= (1<<9) - 1;
rcsmall[i]= (1<<9) - 1;
}
}
void read(){
for(int i = 1;i<= 9;i++){
for(int j = 1;j<= 9 ;j++){
scanf("%d",&sd[i][j]);
}
}
}
void pre(){
for(int i = 1;i<= 9 ;i++){
for(int j =1; j<= 9; j++){
int no = rcno[i][j];
if(sd[i][j] != 0){
row[i] ^= 1<< (sd[i][j]-1);
col[j] ^= 1<< (sd[i][j]-1);
rcsmall[no] ^= 1<< (sd[i][j]-1);
}
else{
cnt ++;
}
}
}
}
int main(){
for(int i =0; i< (1 << 9);i++)
for(int j = i; j; j = j - (j & -j))
onenum[i] ++;
for(int i = 1;i<= 9 ;i ++){
num[1<< (i-1)] = i;
}
cnt = 0;
init();
read();
pre();
dfs(1);
if(flag)
cout << ans;
else cout << "-1";
return 0;
}
好吧,終於還是咬咬牙把前幾天一口氣用bitset+搜索的方式寫完且TLE程序給改AC了,也終於很快完成了靶形數獨,在幾年前不敢寫的程序現在看來也不過如此,確實成長了,但是速度嘛不言而喻,就像我跑馬拉松,人家早就在4小時內到達了終點,我設定的目標是6小時內完賽,享受沿途風景,感受途中心情變化,沒人催,就靠點自律,自己感動自己,跑起來自然那就叫一個慢!