UVA439 骑士的移动

Walker_V

2020-11-02 00:41:48

题解

UVA439 骑士的移动

之所以这道题我要写题解,是因为解题的过程中我采用了多种方法(不严谨的说,基本写完了搜索里的所有技巧)——BFS,IDA* ,A*,双向DFS。

这个过程很值得品味参考,于我来说也是一次不可多得的学习。

BFS

这道题的BFS思路是比较显然的,代码实现上也不算特别难。

#include<bits/stdc++.h>
#define Debug  printf("OK\n");

int f[10][10],dx[9]={0,-1,-2,-2,-1,1,2,2,1},dy[9]={0,2,1,-1,-2,-2,-1,1,2};
char op[5],ed[5];

struct node {
    int x,y;
}s,t;

namespace WalkerV {
    void Init() {
        memset(f,0,sizeof(f));
        return;
    }
    void Solve() {
        s=(node){op[0]-'a'+1,op[1]-'0'},t=(node){ed[0]-'a'+1,ed[1]-'0'};
        std::queue<node> q;
        q.push(s),f[s.x][s.y]=0;
        while(q.size()>0) {
            node k=q.front();
            q.pop();
            if(k.x==t.x&&k.y==t.y) {
                return;
            }
            for(int i=1;i<=8;i++) {
                if(k.x+dx[i]>=1&&k.x+dx[i]<=8&&k.y+dy[i]>=1&&k.y+dy[i]<=8&&!f[k.x+dx[i]][k.y+dy[i]]) {
                    f[k.x+dx[i]][k.y+dy[i]]=f[k.x][k.y]+1;
                    q.push((node){k.x+dx[i],k.y+dy[i]});
                }
            }
        }
        return;
    }

    void Print() {
        printf("To get from %s to %s takes %d knight moves.\n",op,ed,f[t.x][t.y]);
        memset(op,0,sizeof(op));
        memset(ed,0,sizeof(ed));
        return;
    }
}

int main()
{
    while(std::cin>>op>>ed) {
        WalkerV::Init();
        WalkerV::Solve();
        WalkerV::Print();
    }
    return 0;
}

IDA*

然后便是IDA*。

如果你问我为什么不先写A*,因为这份代码的重点是迭代加深,也就是ID(Iterative Deepening)。但单打一份迭代加深的DFS意义不大,所以就选择用IDA*。而且从各种角度来说,IDA*都比A*要优秀一些。

但是这份代码应该说是几份代码我调得最痛苦的一份(大概前前后后修了三四个小时左右),最后的关键错误还是落在了估价函数上。

有必要说一下估价函数的设计:因为是马在棋盘上的移动,所以考虑曼哈顿距离。我们知道马每次移动的曼和顿距离为3,所以乐观估计应当是当前位置到终点的曼哈顿距离除以3并向上取整。

#include<bits/stdc++.h>

int max_dep;
int f[10][10],dx[9]={0,-1,-2,-2,-1,1,2,2,1},dy[9]={0,2,1,-1,-2,-2,-1,1,2};
char op[5],ed[5];

struct node {
    int x,y;
}s,t;

namespace WalkerV {
    void Init() {
        max_dep=0;
        memset(f,0,sizeof(f));
        return;
    }

    double Estimate_Function(int x1,int y1,int x2,int y2) {
        return std::ceil((std::abs(x1-x2)+std::abs(y1-y2))/3.0);
    }

    bool IDDFS(int x,int y,int dep) {
        if(dep==max_dep) {
            if(x==t.x&&y==t.y) {
                f[x][y]=dep;
                return true;
            }
            else {
                return false;
            }
        }
        //printf("x:%d y:%d g(x):%d f(x):%.1f\n",x,y,dep,Estimate_Function(x,y,t.x,t.y));
        for(int i=1;i<=8;i++) {
            if(x+dx[i]>=1&&x+dx[i]<=8&&y+dy[i]>=1&&y+dy[i]<=8&&!f[x+dx[i]][y+dy[i]]) {
                //printf("move to %d %d, g:%d f:%.1f max_dep:%d\n",x+dx[i],y+dy[i],dep+1,Estimate_Function(x+dx[i],y+dy[i],t.x,t.y),max_dep);
                if(dep+1+Estimate_Function(x+dx[i],y+dy[i],t.x,t.y)>max_dep) { //g(x):dep+1 f(x):ceil(M_Dis/3)
                    //printf("cut\n");
                    continue;
                }
                if(IDDFS(x+dx[i],y+dy[i],dep+1)) {
                    return true;
                }
            }
        }
        return false;
    }

    void Solve() {
        s=(node){op[0]-'a'+1,op[1]-'0'},t=(node){ed[0]-'a'+1,ed[1]-'0'};
        while(!IDDFS(s.x,s.y,0)) {
            memset(f,0,sizeof(f));
            max_dep++;
            //printf("max_dep:%d\n",max_dep);
        }
        return;
    }

    void Print() {
        printf("To get from %s to %s takes %d knight moves.\n",op,ed,f[t.x][t.y]);
        memset(op,0,sizeof(op));
        memset(ed,0,sizeof(ed));
        return;
    }

}

int main()
{
    while(std::cin>>op>>ed) {
        WalkerV::Init();
        WalkerV::Solve();
        WalkerV::Print();
    }
    return 0;
}

A*

在有了BFS和IDA*的基础上,A*就显得非常容易了。

估价函数的设计与IDA*是一致的。

#include<bits/stdc++.h>

int f[10][10],dx[9]={0,-1,-2,-2,-1,1,2,2,1},dy[9]={0,2,1,-1,-2,-2,-1,1,2};
char op[5],ed[5];

struct node {
    int x,y;
    double est;
    friend bool operator < (node a,node b) {
        return a.est>b.est;
    }
}s,t;

namespace WalkerV {
    void Init() {
        memset(f,0,sizeof(f));
        return;
    }

    double Estimate_Function(int x,int y) {
        return std::ceil((std::abs(x-t.x)+std::abs(y-t.y))/3.0);
    }

    void Solve() {
        s=(node){op[0]-'a'+1,op[1]-'0',0},t=(node){ed[0]-'a'+1,ed[1]-'0',0};
        s.est=Estimate_Function(s.x,s.y);
        std::priority_queue<node> q;
        q.push(s),f[s.x][s.y]=0;
        while(q.size()) {
            node k=q.top();
            q.pop();
            if(k.x==t.x&&k.y==t.y) {
                return;
            }
            for(int i=1;i<=8;i++) {
                if(k.x+dx[i]>=1&&k.x+dx[i]<=8&&k.y+dy[i]>=1&&k.y+dy[i]<=8&&!f[k.x+dx[i]][k.y+dy[i]]) {
                    f[k.x+dx[i]][k.y+dy[i]]=f[k.x][k.y]+1;
                    q.push((node){k.x+dx[i],k.y+dy[i],f[k.x+dx[i]][k.y+dy[i]]+Estimate_Function(k.x+dx[i],k.y+dy[i])});
                }
            }
        }
        return;
    }

    void Print() {
        printf("To get from %s to %s takes %d knight moves.\n",op,ed,f[t.x][t.y]);
        memset(op,0,sizeof(op));
        memset(ed,0,sizeof(ed));
        return;
    }
}

int main()
{
    while(std::cin>>op>>ed) {
        WalkerV::Init();
        WalkerV::Solve();
        WalkerV::Print();
    }
    return 0;
}

双向BFS

对于这题,由于我们已经知道搜索的初态和末态,所以既可以从初态搜到末态,也可以从末态搜到初态。

在此基础上,双向BFS的思路也就呼之欲出了——从初态和末态同时往中间搜索(具体实现应当是正反各搜一轮)。

由于我们使用的是BFS,所以在轮流搜索的时候但凡有一个状态被两边都搜索到了(也就是说第一个被两边都搜索到的状态),那么这个状态到初态和末态的路径和就是答案(正确性显然)。

此外,在代码中需要注意的有如下两点:

说句闲话,其实中间两个队列的实现那里,如果用typedef写会更好看一些,但是可读性会大大降低(逃

#include<bits/stdc++.h>

int ans;
int f1[10][10],f2[10][10],dx[9]={0,-1,-2,-2,-1,1,2,2,1},dy[9]={0,2,1,-1,-2,-2,-1,1,2};
char op[5],ed[5];

struct node {
    int x,y;
}s,t;

namespace WalkerV {
    void Init() {
        memset(f1,0,sizeof(f1));
        memset(f2,0,sizeof(f2));
        return;
    }

    void Solve() {
        s=(node){op[0]-'a'+1,op[1]-'0'},t=(node){ed[0]-'a'+1,ed[1]-'0'};
        if(s.x==t.x&&s.y==t.y) {
            ans=0;
            return;
        }
        std::queue<node> q1,q2;
        q1.push(s),f1[s.x][s.y]=0;
        q2.push(t),f2[t.x][t.y]=1;
        while(q1.size()||q2.size()) {
            bool flag;
            node k;
            if(q1.size()<=q2.size()) { //edit q1
                k=q1.front();
                q1.pop();
                flag=true;
            }
            else { //edit q2
                k=q2.front();
                q2.pop();
                flag=false;
            }
            for(int i=1;i<=8;i++) {
                if(k.x+dx[i]>=1&&k.x+dx[i]<=8&&k.y+dy[i]>=1&&k.y+dy[i]<=8) {
                    switch(flag) {
                        case true:
                            if(f2[k.x+dx[i]][k.y+dy[i]]) {
                                ans=f1[k.x][k.y]+f2[k.x+dx[i]][k.y+dy[i]];
                                return;
                            }
                            f1[k.x+dx[i]][k.y+dy[i]]=f1[k.x][k.y]+1;
                            q1.push((node){k.x+dx[i],k.y+dy[i]});
                            break;
                        case false:
                            if(f1[k.x+dx[i]][k.y+dy[i]]) {
                                ans=f2[k.x][k.y]+f1[k.x+dx[i]][k.y+dy[i]];
                                return;
                            }
                            f2[k.x+dx[i]][k.y+dy[i]]=f2[k.x][k.y]+1;
                            q2.push((node){k.x+dx[i],k.y+dy[i]});
                            break;
                    }               
                }
            }
        }
        return;
    }

    void Print() {
        printf("To get from %s to %s takes %d knight moves.\n",op,ed,ans);
        memset(op,0,sizeof(op));
        memset(ed,0,sizeof(ed));
        return;
    }
}

int main()
{
    while(std::cin>>op>>ed) {
        WalkerV::Init();
        WalkerV::Solve();
        WalkerV::Print();
    }
    return 0;
}

说点别的

这次这道题码下来,还算是收获颇丰。谁会想到,一个学了两年多OI的人,直到这道题才算是摸透了上面四种算法(包括BFS)。

此外,在写代码的时候,感觉到(ID)A*是真正优秀的算法。这不仅体现在它的运行时间上,而是说这种算法是最贴近人类行为的。作为人类,我们在解决问题中面临多种方式时,一定会略作估计并选取最优的方式进行尝试。可能这也是启发式算法被广泛运用到人工智能里的原因,毕竟我们人类定义的人工智能就是与人类智能所相似的机器。