八数码问题 解的存在性证明以及解法分析(附代码)

问题描述:

很经典的问题,给一个4399的网址,自己玩一下就知道了。。。。。
8数码小游戏试玩

分析:

一 . 基础BFS

最显然想到肯定是暴力搜索,因为我希望找到一个最少操作步数的解决方案,所以比较明显的就是可以用最基本的广度优先级搜索,因为我的状态一共只有 9! 种,所以即使搜遍所有状态还是很快的。
那么如何判断某一状态是否已经搜过了呢,还是用标记数组。显然就是把 0~8 的每一个排列(0表示空位)映射到 0 ~ 9!-1 的每一个数字,这个康托展开就能实现了,提取也就是解压出排列可以进行康拓展开的逆运算,也不是很麻烦。
因为我以为可以解决 4×4 的棋盘的问题,16! 种状态,所以没有用标记数组来表示,而是用的map,既记录了每个状态的上一状态,方便最后输出路径,也标记了每个状态是否曾经遍历过。但是实测只能解决所有 3×3 规模的问题和极少极少极少4×4规模的搜索。

首先献上最初级的暴力Bfs的代码,因为以后肯定要用别的方法解决更大规模的问题,所以该方法的代码也就没有再进一步在一些细节的地方优化速度。

暴力BFS:

#include<bits/stdc++.h>
using namespace std;
#define Bit 3
int n=Bit*Bit;
long long int fio[100]={0};
void Init_fio()
{
    fio[0]=1;
    for(int i=1;i<100;i++)fio[i]=fio[i-1]*i;
}
struct Map
{
    int a[Bit*Bit+1];
    int pos;//可移动点0的位置
    long long int val;//对应的状态值
}s,t;
long long int Get_val(Map x)//地图x的状态压缩
{
    long long int ret=0;
    for(int i=0;i<n;i++)
    {
        int cnt=0;
        for(int j=i+1;j<n;j++)
            if(x.a[j]<x.a[i])cnt++;
        ret+=cnt*fio[n-i-1];//fio[]为阶乘
    }
    //printf("%lld\n",ret);
    return ret;//返回该字符串是全排列中第几大,从0开始
}
Map Get_Map(long long int x)//状态x的康拓展开对应地图,同时找到0的位置
{
    Map ret;
    bool vis[Bit*Bit+10];//标记
    memset(vis,0,sizeof(vis));
    for(int i=0;i<n;i++)
    {
        int temp=x/fio[n-i-1];
        for(int j=0;j<=temp;j++)
            if(vis[j])temp++;
        ret.a[i]=temp;
        if(temp==0)ret.pos=i;
        vis[temp]=1;
        x%=fio[n-i-1];
    }
    return ret;
}
void Get_s()//输入起点
{
    for(int i=0;i<n;i++)
    {
        scanf("%d",&s.a[i]);
        if(s.a[i]==0) s.pos=i;
    }
    s.val=Get_val(s);
}
void Get_t()//输入终点
{
    for(int i=0;i<n-1;i++)
    {
        t.a[i]=i+1;
    }
    t.a[n-1]=0;
    t.pos=n-1;
    t.val=Get_val(t);
}
bool Inside(int x,int y)
{
    if(x<0||x>=Bit||y<0||y>=Bit)return 0;
    return 1;
}
void Output_way(long long int x,map<long long int,long long int>Last_point)//从状态x回溯之前所有状态路径
{
    stack<long long int>a;
    while(x!=-1)
    {
        a.push(x);
        x=Last_point[x];
    }
    Map now=Get_Map(a.top());
    while(a.top()!=t.val)
    {
        int tx=now.pos%Bit , ty=now.pos/Bit;
        a.pop();
        now=Get_Map(a.top());
        printf("%d  ",now.a[ty*Bit+tx]);
    }
}
void Bfs()
{
    int mov[4][2]={{1,0},{-1,0},{0,1},{0,-1}},flag=0;
    queue<long long int>q;
    map<long long int,long long int>Last_point;
    q.push(s.val);
    Last_point[s.val]=(long long int)(-1);
    while(!q.empty())
    {
        if(flag==1)break;
        Map now=Get_Map(q.front());
        now.val=q.front();
        int now_x=now.pos%Bit , now_y=now.pos/Bit;
        for(int i=0;i<4;i++)
        {
            int next_x=now_x+mov[i][0] , next_y=now_y+mov[i][1];
            if(Inside(next_x,next_y))//如果可以向该方向移动
            {
                Map Next=now;
                swap(Next.a[next_y*Bit+next_x],Next.a[now_y*Bit+now_x]);
                Next.val=Get_val(Next);
                if(Last_point.count(Next.val)!=0)continue;
                Last_point[Next.val]=now.val;
                if(Next.val==t.val)
                {
                    Output_way(Next.val,Last_point);
                    flag=1;
                    break;
                }
                q.push(Next.val);
            }
        }
        q.pop();
    }
}
int main()
{
    Init_fio();
    Get_s();
    Get_t();
    Bfs();
    while(1)getchar();
}

二 . 双向BFS搜索

因为起始状态和目标状态都是已知的,所以我们完全可以从起始状态 s 和目标状态 t 两个状态开始同时 BFS,这样,不仅减少了所需空间,同时肯定也减少了搜索的状态数。虽然在判断是否搜到重复状态的时候,时间由之前的 O(1) 直接判断变成了 map 的 O(logn) ,但是因为需要搜索的状态少了,map也小了,所以在这方面产生的时间的增加可以接受。而且可以极大地减少搜索的状态数。

下面分析双向 BFS 和暴力单起点 BFS 相比少搜索的状态的数目:
网上很多人说“画一个状态的树形图可以看出状态数从一个三角形变成了一个菱形,就估计求解时搜索到的状态数目只减少了一半”。其实我觉得这个是很不科学的,我们想象一个网格图,从左上角为起点,每个点只向和它相邻的右边和下边两个方格扩展染色,那么,它扩展到某一格点(x,y)的时候,被染出的三角形的面积恰好是二倍的双向扩展时得到的矩形面积。那么我们把这个情况对应成一个状态树的搜索,那么这个状态树只是一个二叉树,并且有很大的重复度(大部分状态都由上一层的两个状态扩展得来)。仅仅在这样的情况下,状态数才变为了原来的一半,那么如果是这个问题的四叉树呢?
我们如果忽略状态重复的问题考虑最差的时间复杂度,那么假如说单向 BFS 搜索需要搜索到第 n 层才能找到结束路径,那么它最多搜索了 4n 4 n 种状态,时间复杂度 O(4n) O ( 4 n ) 。那么如果是双向搜索呢,显然只需要每个方向搜索 n2 n 2 步即可,那么他就只搜索了 24n2 2 ∗ 4 n 2 种状态,时间复杂度 O(4n2) O ( 4 n 2 ) 。时间为单向 BFS 的 12 1 2 次方,如此看来,这种双向搜索的方法对于目标状态已知并且搜索过程中重复状态数较为稀疏的情况下,确实是极大的加大了效率。

下面附上双向搜索的代码,实测对于八数码问题任何有解的情况的求解,都可以在一瞬间得到对应答案。但是对于更大规模的十五数码的问题,仍然比较无能为力。只有极少情况下可以在一分钟内求解出十五数码的解。

//八数码问题双向Bfs搜索
#include<bits/stdc++.h>
using namespace std;
#define Bit 3
int n=Bit*Bit;
long long int fio[100]={0};
int Bfs_time_s,Bfs_time_t;
void Init_fio()
{
    fio[0]=1;
    for(int i=1;i<100;i++)fio[i]=fio[i-1]*i;
}
struct Map
{
    int a[Bit*Bit+1];
    int pos;//可移动点0的位置
    long long int val;//对应的状态值
}s,t;
long long int Get_val(Map x)//地图x的状态压缩
{
    long long int ret=0;
    for(int i=0;i<n;i++)
    {
        int cnt=0;
        for(int j=i+1;j<n;j++)
            if(x.a[j]<x.a[i])cnt++;
        ret+=cnt*fio[n-i-1];//fio[]为阶乘
    }
    //printf("%lld\n",ret);
    return ret;//返回该字符串是全排列中第几大,从0开始
}
Map Get_Map(long long int x)//状态x的康拓展开对应地图,同时找到0的位置
{
    Map ret;
    bool vis[Bit*Bit+10];//标记
    memset(vis,0,sizeof(vis));
    for(int i=0;i<n;i++)
    {
        int temp=x/fio[n-i-1];
        for(int j=0;j<=temp;j++)
            if(vis[j])temp++;
        ret.a[i]=temp;
        if(temp==0)ret.pos=i;
        vis[temp]=1;
        x%=fio[n-i-1];
    }
    return ret;
}
void Get_s()//输入起点
{
    for(int i=0;i<n;i++)
    {
        scanf("%d",&s.a[i]);
        if(s.a[i]==0) s.pos=i;
    }
    s.val=Get_val(s);
}
void Get_t()//输入终点
{
    for(int i=0;i<n-1;i++)
    {
        t.a[i]=i+1;
    }
    t.a[n-1]=0;
    t.pos=n-1;
    t.val=Get_val(t);
}
bool Inside(int x,int y)
{
    if(x<0||x>=Bit||y<0||y>=Bit)return 0;
    return 1;
}
void Output_way(long long int x,map<long long int,long long int>Last_point_s,
                map<long long int,long long int>Last_point_t)//从状态x回溯之前所有状态路径
{
    //printf("7s");
    stack<long long int>a;
    long long int temp=x;
    while(temp!=-1)
    {
        a.push(temp);
        temp=Last_point_s[temp];
    }
    Map now=Get_Map(a.top());
    while(a.top()!=x)
    {
        int tx=now.pos%Bit , ty=now.pos/Bit;
        a.pop();
        now=Get_Map(a.top());
        printf("%d  ",now.a[ty*Bit+tx]);
    }
    now=Get_Map(x);//printf("sda");
    while(x!=t.val)
    {
        int tx=now.pos%Bit , ty=now.pos/Bit;
        x=Last_point_t[x];
        now=Get_Map(x);
        printf("%d  ",now.a[ty*Bit+tx]);
    }
    printf("\n");
}
void Double_Bfs()
{
    int mov[4][2]={{1,0},{-1,0},{0,1},{0,-1}};
    queue<long long int>q,p;//q为从s搜索,p为从t搜索
    map<long long int,long long int>Last_point_s;//存以s为起点的搜索路径
    map<long long int,long long int>Last_point_t;//存以t为起点的搜索路径
    q.push(s.val);
    p.push(t.val);
    Last_point_s[s.val]=(long long int)(-1);
    Last_point_t[t.val]=(long long int)(-1);
    while(!q.empty()&&!p.empty())
    {
        Map now=Get_Map(q.front());
        now.val=q.front();
        int now_x=now.pos%Bit , now_y=now.pos/Bit;
        for(int i=0;i<4;i++)
        {
            int next_x=now_x+mov[i][0] , next_y=now_y+mov[i][1];
            if(Inside(next_x,next_y))//如果可以向该方向移动
            {
                Map Next=now;
                swap(Next.a[next_y*Bit+next_x],Next.a[now_y*Bit+now_x]);
                Next.val=Get_val(Next);
                if(Last_point_s.count(Next.val)!=0)continue;
                Bfs_time_s++;
                Last_point_s[Next.val]=now.val;
                if(Last_point_t.count(Next.val)!=0)
                {
                    Output_way(Next.val,Last_point_s,Last_point_t);
                    return;
                }
                q.push(Next.val);
            }
        }
        q.pop();

        now=Get_Map(p.front());
        now.val=p.front();
        now_x=now.pos%Bit , now_y=now.pos/Bit;
        for(int i=0;i<4;i++)
        {
            int next_x=now_x+mov[i][0] , next_y=now_y+mov[i][1];
            if(Inside(next_x,next_y))//如果可以向该方向移动
            {
                Map Next=now;
                swap(Next.a[next_y*Bit+next_x],Next.a[now_y*Bit+now_x]);
                Next.val=Get_val(Next);
                if(Last_point_t.count(Next.val)!=0)continue;
                Bfs_time_t++;
                Last_point_t[Next.val]=now.val;
                if(Last_point_s.count(Next.val)!=0)
                {
                    Output_way(Next.val,Last_point_s,Last_point_t);
                    return;
                }
                p.push(Next.val);
            }
        }
        p.pop();
    }
    //printf("***");
}
void Time_test()
{
    srand( (unsigned)time( NULL ) );
    Init_fio();
    for(int i=0;i<100;i++)
    {
        Bfs_time_s=0,Bfs_time_t=0;
        s=Get_Map(rand()%fio[n]);
        s.val=Get_val(s);
        Get_t();
        if(s.val==t.val){i--;continue;}
        Double_Bfs();
        printf("\n%d  %d\tTs=%d\tTt=%d\n",s.val,t.val,Bfs_time_s,Bfs_time_t);
    }
}
int main()
{
    Init_fio();
    int k=0;
    while(scanf("%d",&k))
    {
        s=Get_Map(k);
        s.val=Get_val(s);
        Get_t();
        Double_Bfs();
    }
    //Time_test();
    /*Init_fio();
    Get_s();
    Get_t();
    Double_Bfs();//假设s和t不同
    while(1)getchar();*/
}

未完待更新~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值