对于数独问题的专题探究
最近研究了一下数独之类的问题(其实就两个)。总结一下:
数独,因为其较为复杂的相互关系被视为一种脑力工作。如果要去用计算机解决这个问题,则被看做一种暴力问题。
对于这些相互关系,我们可以用若干个二维数组去存储。
比如说P1784 数独,这一题,我们只需要用一个数组 \(h_{i,num}\) 去表示第 \(i\) 行可否放 \(num\) 数字,用一个数组 \(l_{i,num}\) 去表示第 \(i\) 列可否放 \(num\) 数字,然后再用一个 \(g_{i,num}\) 来表示第 \(i\) 个九宫格中,可不可以放 \(num\) 数字。
其实,在没看题解之前,我是用的一个三维数组描述可否放置的关系的,我是用的 \(b_{i,j,num}\) 来表示在 \((i,j)\) 这个位置上,可不可以放置 \(num\) 的。这种方法为什么不好。仔细思考后会发现,一个 \(b_{i,j,num}\) 是关乎着它的行和列的,所以说,有很多地方可以更改这个数据,当我回溯的时候我并不知道这一个是我更改过的吗,所以这种方法并不是很好。同时,这种方法对于九宫格的逐一打标记是比较繁琐的。
好的,讲完了理论部分,那么开始实践部分吧。
先从一道黄题开始。题目传送门
直截了当,就是数独。
抱着侥幸的心态,没有写优化,直接上,结果AC了,其实这道题目并不是特别需要优化。
那好,直接上die码:
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
template <typename T>inline void read(T& t){
t=0; register char ch=getchar();
while(!('0'<=ch&&ch<='9')){if(ch=='-') t=-1;ch=getchar();}
while(('0'<=ch&&ch<='9')){t=((t<<1)+(t<<3))+ch-'0'; ch=getchar();}
}
template <typename T,typename... Args> inline void read(T& t, Args&... args){
read(t);read(args...);
}
template <typename T>inline void write(T x){
if(x<0) putchar('-'),x=~(x-1); int s[40],top=0;
while(x) s[++top]=x%10,x/=10; if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
}
const int n=9;
int h[11][11],l[11][11],g[11][11],a[11][11];
inline int gg(int x,int y){
return (x-1)/3*3+(y-1)/3+1;
}
void Print(){
for(int i=1;i<=n;++i){
for(int j=1;j<=n;++j)
cout<<a[i][j]<<' ';
cout<<endl;
}
exit(0);
}
void dfs(int x,int y){
if(x>n) dfs(1,y+1);
if(y>n){
Print();
exit(0);
}
if(a[x][y]){
dfs(x+1,y);
return;
}
for(int num=1;num<=9;++num){
if(h[x][num]&&l[y][num]&&g[gg(x,y)][num]){
a[x][y]=num;
h[x][num]=l[y][num]=g[gg(x,y)][num]=0;
dfs(x+1,y);
h[x][num]=l[y][num]=g[gg(x,y)][num]=1;
a[x][y]=0;
}
}
}
int main(){
memset(h,1,sizeof(h));
memset(l,1,sizeof(l));
memset(g,1,sizeof(g));
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j){
cin>>a[i][j];
if(a[i][j]!=0) h[i][a[i][j]]=l[j][a[i][j]]=g[gg(i,j)][a[i][j]]=0;
}
dfs(1,1);
return 0;
}
//(x-1)/3*3+(y-1)/3+1
这个题目只要求搜到满足条件的唯一一个解就ok了,所以时间复杂度不是那么假。在中途就可能终止程序。\(79ms\) 其实很快了。
根据这个思路,顺着出题者的尿性思路,我们可以稍微调整一下搜索顺序,原来是对于每一位的思考,从1到9,那么我偏偏反着来,从9到1。
其实数据随机根本优化不了多少,但是这题优化的比较多吧。一个点优化了50ms
总计 \(21ms\) 。
好,现在上第二题,循序渐进,第二题来个蓝题:题目传送门
其实感觉难度虚高。
对于这一题,我们其实只需要搜完,然后按照答案的方式取一个最大值。但是剪枝,以及搜索顺序的思考要考虑完整。这题感觉没有最优性剪枝等,所以主要是从搜索顺序上优化。
无优化
这一种方法简单,就是上一题的加权求答案版本。但是过了这一题(非常勉强)。
不多说,直接上die码:、
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
template <typename T>inline void read(T& t){
t=0; register char ch=getchar();
while(!('0'<=ch&&ch<='9')){if(ch=='-') t=-1;ch=getchar();}
while(('0'<=ch&&ch<='9')){t=((t<<1)+(t<<3))+ch-'0'; ch=getchar();}
}
template <typename T,typename... Args> inline void read(T& t, Args&... args){
read(t);read(args...);
}
template <typename T>inline void write(T x){
if(x<0) putchar('-'),x=~(x-1); int s[40],top=0;
while(x) s[++top]=x%10,x/=10; if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
}
inline int gg(int x,int y){
return (x-1)/3*3+(y-1)/3+1;
}
bool h[11][11],l[11][11],g[11][11];
vector<int>p;
const int n=9;
int a[11][11];
int ans=0;
int xs[11][11]={{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}};
void dfs(int x,int y,int score){
if(x>n) dfs(1,y+1,score);
if(y>n){
ans=max(score,ans);
return;
}
if(a[x][y]){dfs(x+1,y,score+a[x][y]*xs[x][y]);
return;}
for(register int num=1;num<=9;++num)
if(h[x][num]&&l[y][num]&&g[gg(x,y)][num]){
//说明这一位可以放上num
h[x][num]=l[y][num]=g[gg(x,y)][num]=0;
dfs(x+1,y,score+num*xs[x][y]);
h[x][num]=l[y][num]=g[gg(x,y)][num]=1;
}
}
int main(){
memset(h,1,sizeof(h));
memset(l,1,sizeof(l));
memset(g,1,sizeof(g));
for(register int i=1;i<=n;++i)
for(register int j=1;j<=n;++j){
read(a[i][j]);
h[i][a[i][j]]=l[j][a[i][j]]=g[gg(i,j)][a[i][j]]=(a[i][j]==0);
}
dfs(1,1,0);
cout<<((ans==0)?-1:ans)<<endl;
return 0;
}
//(x-1)/3*3+(y-1)/3+1
这一种方法跑的非常慢,差一点点TLE 测评记录
Way1 行优化
这一种有很多思考的点。
为什么这种优化会优化。很多谷民都提出了这个问题,经过一段时间的研究后,我得出了原因所在。
假如说,现在我只要填一个 \(3×3\) 的数独,如下图。
好的,现在这个图,肯定最少数字的点肯定是第一行这个,如果我现在填上了它,第二行第二个的也可以确定下来。所以说,这种思路是比较快的。
主要的误区在于要从填写这个数独的角度,而不是分数的角度考虑。
所以说,这种算法是比较快的(\(966ms\))。
值得一提的是,我同样用了上面一题的方法(改变搜索顺序)尝试,其实是变慢了( \(977ms\) ),按道理不会变慢,可能是评测姬波动。但是,其实全部搜索结束的时候,搜索顺序的变化是没有用的。
下面上die码:
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
template <typename T>inline void read(T& t){
t=0; register char ch=getchar();
while(!('0'<=ch&&ch<='9')){if(ch=='-') t=-1;ch=getchar();}
while(('0'<=ch&&ch<='9')){t=((t<<1)+(t<<3))+ch-'0'; ch=getchar();}
}
template <typename T,typename... Args> inline void read(T& t, Args&... args){
read(t);read(args...);
}
template <typename T>inline void write(T x){
if(x<0) putchar('-'),x=~(x-1); int s[40],top=0;
while(x) s[++top]=x%10,x/=10; if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
}
inline int gg(int x,int y){
return (x-1)/3*3+(y-1)/3+1;
}
struct Info{
int x,s;
bool operator <(const Info &info) const {
return s<info.s;
}
};
bool h[11][11],l[11][11],g[11][11];
const int n=9;
int a[11][11];
vector<Info>p;
int ans=0;
int xs[11][11]={{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}};
void dfs(int x,int y,int score,int dep){
if(y>n) dfs(p[dep+1].x,1,score,dep+1);
if(dep==9){
ans=max(ans,score);
return;
}
if(a[x][y]){
dfs(x,y+1,score+a[x][y]*xs[x][y],dep);
return;
}
for(register int num=1;num<=9;++num)
if(h[x][num]&&l[y][num]&&g[gg(x,y)][num]){
//说明这一位可以放上num
h[x][num]=0;l[y][num]=0;g[gg(x,y)][num]=0;
dfs(x,y+1,score+num*xs[x][y],dep);
h[x][num]=1;l[y][num]=1;g[gg(x,y)][num]=1;
}
}
int main(){
memset(h,1,sizeof(h));
memset(l,1,sizeof(l));
memset(g,1,sizeof(g));
for(register int i=1;i<=n;++i)
for(register int j=1;j<=n;++j){
read(a[i][j]);
h[i][a[i][j]]=l[j][a[i][j]]=g[gg(i,j)][a[i][j]]=(a[i][j]==0);
}
for(register int i=1;i<=n;++i){
int sum=0;
for(register int j=1;j<=n;++j){
for(register int k=1;k<=n;++k)
if(h[i][k]&&l[j][k]&&g[gg(i,j)][k]) ++sum;
}
p.push_back({i,sum});
}
sort(p.begin(),p.end());
dfs(p[0].x,1,0,0);
cout<<((ans==0)?-1:ans)<<endl;
return 0;
}
//(x-1)/3*3+(y-1)/3+1
Way2:从输入的时候填写方法少的地方开始搜索
注意,这种方法并不是正解,TLE90pts
这种方法很假,这里讲一下,为什么会假。首先,对于这种方法,并没有动态性,就是说,我一开始填写方法少,并不代表后来填写的方法会少,所以说这种方法并不是很好。
并且,这种方式其实和随机搜索是差不多的,还加上了一开始处理的常数,其实是一种错解。
这里出示die码,仅供思考。
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
template <typename T>inline void read(T& t){
t=0; register char ch=getchar();
while(!('0'<=ch&&ch<='9')){if(ch=='-') t=-1;ch=getchar();}
while(('0'<=ch&&ch<='9')){t=((t<<1)+(t<<3))+ch-'0'; ch=getchar();}
}
template <typename T,typename... Args> inline void read(T& t, Args&... args){
read(t);read(args...);
}
template <typename T>inline void write(T x){
if(x<0) putchar('-'),x=~(x-1); int s[40],top=0;
while(x) s[++top]=x%10,x/=10; if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
}
inline int gg(int x,int y){
return (x-1)/3*3+(y-1)/3+1;
}
struct Info{
int x,y,s;
bool operator <(const Info &info) const {
return s<info.s;
}
};
bool h[11][11],l[11][11],g[11][11];
const int n=9;
int a[11][11];
vector<Info>p;
int ans=0;
int xs[11][11]={{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}};
void dfs(int score,int dep){
if(dep==81){
ans=max(ans,score);
return;
}
int x=p[dep].x,y=p[dep].y;
if(a[x][y]){dfs(score+a[x][y]*xs[x][y],dep+1);
return;}
for(register int num=1;num<=9;++num)
if(h[x][num]&&l[y][num]&&g[gg(x,y)][num]){
//说明这一位可以放上num
h[x][num]=0;l[y][num]=0;g[gg(x,y)][num]=0;
dfs(score+num*xs[x][y],dep+1);
h[x][num]=1;l[y][num]=1;g[gg(x,y)][num]=1;
}
}
int main(){
memset(h,1,sizeof(h));
memset(l,1,sizeof(l));
memset(g,1,sizeof(g));
for(register int i=1;i<=n;++i)
for(register int j=1;j<=n;++j){
read(a[i][j]);
h[i][a[i][j]]=l[j][a[i][j]]=g[gg(i,j)][a[i][j]]=(a[i][j]==0);
}
for(register int i=1;i<=n;++i){
for(register int j=1;j<=n;++j){
int sum=0;
for(register int k=1;k<=n;++k)
if(h[i][k]&&l[j][k]&&g[gg(i,j)][k]) ++sum;
p.push_back({i,j,sum});
}
}
sort(p.begin(),p.end());
dfs(0,0);
cout<<((ans==0)?-1:ans)<<endl;
return 0;
}
//(x-1)/3*3+(y-1)/3+1