对于数独问题的专题探究

最近研究了一下数独之类的问题(其实就两个)。总结一下:

数独,因为其较为复杂的相互关系被视为一种脑力工作。如果要去用计算机解决这个问题,则被看做一种暴力问题。

对于这些相互关系,我们可以用若干个二维数组去存储。

比如说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
posted @ 2021-12-03 20:21  Mercury_City  阅读(165)  评论(0编辑  收藏  举报