我是最快的马

描述

我们都知道,在中国象棋中,马是走日字步的。现给定马的起始坐标与终点坐标,求出马最快能到达的路线。如果有多条路线都是步数最少的,则输出路线的数目
注意,此时棋盘上可能会有一些其它的棋子,这些棋子是会憋马脚的,注意!

输入

前两行为起点坐标与终点坐标,第三行为一个数字M,之后的M行为棋盘上其它棋子的坐标(M<32,坐标行列值<=10)

输出

如果最快路线唯一,则输出路线。否则只输出一个数字,为最快路线的数目N

样例输入

0 0
2 4
1
1 1

样例输出

(0,0)-(1,2)-(2,4)

背景

本人在oj上做到了这道作业题,一开始做的过程产生了一些困难,主要是不知道怎么记录多条最优路线。这也是这道题比普通BFS稍难一些的点。

在搜索CSDN后发现找不到这道题,在百度上搜到了一篇题解,但是他处理如何记录多条最短路径的思路是错误的,由于oj上这题的测试数据太弱了,所以他的错误做法也能通过。我看到有大概十几个人也在CSDN上搜索过这道题,而且贵校数算历年作业题好像不怎么改,所以写下这篇题解,希望能帮助到同学和之后的学弟学妹。(PKU2021届的同学做这道题的时候尽量不要直接复制代码,我不知道万一作业查重了会不会对我有处罚orz)

思路

首先显然是要用BFS,其次他要求输出路径,所以要用数组模拟队列,而不是直接用STL里面的queue。这些都是一看到题目就能想到的,不做赘述。

这里我想主要探讨一下如何记录多条最短路径。如果用传统的visited二维数组,记录0为未访问,1为访问过,会造成一个问题是:在逐层搜索的时候,假如我们要扩展节点n的下一层节点m,那么会将visited[m]置为1,如果有和n同一层的其他节点再想访问m时就不会访问了,这样有不同的路径我们也不知道了。语言不太好描述,不如看图:

 

 (抱歉字有点丑)那么如何解决这个问题呢?

我们可以改变visited数组的逻辑,不再用01记录,而是未访问的设为-1,访问过的设为被访问时候的层数。在判重的时候,判断当前节点如果未访问或者是其父节点的层数加1都不算重复。采用这种思路可以达到传统判重方法一样的效果,即同层的兄弟节点和父亲节点都不会被访问,未访问过的节点会被访问,此外同层兄弟节点访问过的节点还会被访问。

最后,在队列中遇到目标节点后我们再查看一下队里还有几个目标节点即可(他们一定是有着不同的路径,不会出现同样路径的目标节点有多个的情况,如何证明留给读者思考)

代码

#include<bits/stdc++.h>
using namespace std;
//节点定义
struct Node{
    int x;
    int y;
    int pre;
    int layer;
    Node(){}
    Node(int xx,int yy,int p,int l):x(xx),y(yy),pre(p),layer(l){}
};

//全局变量
int vis[11][11];
int pie[11][11];
int step[8][2]={{-2,1},{-1,2},{1,2},{2,1},{2,-1},{1,-2},{-1,-2},{-2,-1}};
Node q[1010];
int head=0,rear=1;
int ansNum=0;
Node ansNode;
Node s,e;

//路径输出函数
void print(Node n){
    stack<Node> sk;
    while(n.pre!=-1){
        sk.push(n);
        n=q[n.pre];
    }
    cout<<'('<<s.x<<','<<s.y<<')';
    while(!sk.empty()){
        cout<<"-("<<sk.top().x<<','<<sk.top().y<<')';
        sk.pop();
    }
    cout<<endl;
}

//主函数
int main(){

    //数据的输入及初始化工作
    cin>>s.x>>s.y>>e.x>>e.y;
    s.pre=-1;
    s.layer=0;
    int n;
    cin>>n;
    for(int i=0;i<n;i++){
        int tmpx,tmpy;
        cin>>tmpx>>tmpy;
        pie[tmpx][tmpy]=1;
    }
    memset(vis,0xff,sizeof(vis));//初始化为-1
    vis[s.x][s.y]=0;
    q[0]=s;

    //BFS部分
    while(head!=rear){
        Node old=q[head];
        int oldX=old.x,oldY=old.y;
        if(oldX==e.x&&oldY==e.y){
            //遇到目标节点,保存下来,并且直接清空队列,数目标节点个数
            ansNode=old;
            ansNum=1;
            while(head!=rear){
                Node tmp=q[++head];
                if(tmp.x==e.x&&tmp.y==e.y&&tmp.layer==ansNode.layer)
                ansNum++;
            }
            if(ansNum==1)
            print(ansNode);
            else
            cout<<ansNum<<endl;
            system("pause");
            return 0;
        }

        //扩展节点并入队
        for(int i=0;i<8;i++){
            int nowX=oldX+step[i][0];
            int nowY=oldY+step[i][1];
            int nowL=old.layer+1;
            int p=head;
            int mX=oldX+step[i][0]/2;
            int mY=oldY+step[i][1]/2;
            if(nowX<11&&nowX>=0&&nowY<11&&nowY>=0
            &&!pie[mX][mY]
            &&(vis[nowX][nowY]==-1||vis[nowX][nowY]==nowL)){
                q[rear++]=Node(nowX,nowY,p,nowL);
                vis[nowX][nowY]=nowL;
            }
        }
        //扩展完的头结点弹出
        head++;
    }

    //若while结束,说明队列已空还没遇到目标节点,在本题中不可能出现这种情况
    //所以我们输出一个报错来提醒自己
    cout<<"error: empty queue"<<endl;
    system("pause");
    return 0;
}

后话

由于oj上数据的薄弱性,虽然本人提供的代码accepted了,但仍可能存在某些纰漏,欢迎评论区指正,或者交流其他更优算法。第一次写文章,语言不甚清晰,有看不懂之处欢迎评论区提问

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值