Loading

Abbott的复仇——UVa 816

有一个最多包含9*9个交叉点的迷宫。输入起点、离开起点时的朝向和终点,求一条最短路(多解时任意输出一个即可)。

这个迷宫的特殊之处在于:进入一个交叉点的方向(用NEWS这4个字母分别表示北东西南,即上右左下)不同,允许出去的方向也不同。例如,1 2 WLF NR ER *表示交叉点(1,2)(上数第1行,左数第2列)有3个路标(字符*只是结束标志),如果进入该交叉点时的朝向为W(即朝左),则可以左转(L)或者直行(F);如果进入时朝向为N或者E则只能右转
(R)。

注意:初始状态是“刚刚离开入口”,所以即使出口和入口重合,最短路也不为空。例如,图中的一条最短路为(3,1) (2,1) (1,1) (1,2) (2,2) (2,3) (1,3) (1,2) (1,1) (2,1) (2,2) (1,2) (1,3) (2,3) (3,3)。

输入输出示例

可能有若干组输入,第一行是该组输入的名字,第二行是起点,初始朝向和终点,第三行开始就是图,一行的输入从*结束。遇到0结束该组输入。

遇到END结束全部输入。

对于每组输入,输出改组的名字和从入口到出口的最短路径,一行最多十个节点。

Sample Input

SAMPLE
3 1 N 3 3
1 1 WL NR *
1 2 WLF NR ER *
1 3 NL ER *
2 1 SL WR NF *
2 2 SL WF ELF *
2 3 SFR EL *
0
NOSOLUTION
3 1 N 3 2
1 1 WL NR *
1 2 NL ER *
2 1 SL WR NFR *
2 2 SR EL *
0
END

Sample Output

SAMPLE
(3,1) (2,1) (1,1) (1,2) (2,2) (2,3) (1,3) (1,2) (1,1) (2,1)
(2,2) (1,2) (1,3) (2,3) (3,3)
NOSOLUTION
No Solution Possible

思路

我他妈没思路。都得看刘汝佳才有思路。淦淦淦淦!!

是一个图的最短路径问题,但这个图不是个一般的图,一个节点,从不同方向进来,能走的路径不一样,比如上图的(2,1)节点如果从南面进来,就只能向北走(直走),从北面进来就只能向东走(左转),从东面进来就只能向北走(右转)。

那就不能建立一个简单的图,因为简单的图无法表示这种关系。这些错综复杂的条件也是这题的难点。

因为每个节点有所在的行和列,还有进入的方向,要出去的方向,所以需要用一个四维数组来存,int have_edge[r][c][d][t],该数组记录的是从dir方向进入第r行第i列的节点时能不能向t方向转。如have_edge[2][1][S][R](下标从1开始,0空着)就是false,因为从南进入(2,1)后只能直走,不能右转。

还有个问题就是,我们要用程序实现,那么方向自然就不能用字符,我们要给它编码成数字,题中规定的方向有东南西北,转向方向有左转,右转和直走,所以我们可以这样定义have_edge[MAX][MAX][4][3],其中MAX是最大节点数,4是因为方向只有四个状态,3是因为转向只有三个状态,我们要把它们分别映射到[0,3][0,2]中去。

const char* dirs = "NESW";
const char* turns = "FLR";
int have_edge[MAX][MAX][4][3];

int dir_id(char c){
    return strchr(dirs, c) - dirs;
}

int turn_id(char c){
    return strchr(turns, c) - turns;
}

strchr(a,b)返回b在a中的第一个位置的指针,所以减去头指针就是剩下的数字,这个数字一定小于a的长度。

这样我们就把方向和转向编码了,分别编成0,1,2,30,1,2

然后就是处理输入数据,我们在这个阶段主要要做的就是把输入数据整理成图。

// en_r,en_c分别代表入口点的行列,en_dir代表入口时的方向
// out_r,out_c代表出口点的行列
// en_r1和en_c1表示从入口点按en_dir走了一步后的位置
// 因为入口点和出口点并不在图里,所以这样模拟下
int scan() {
    int r, c;
    char dt[4];
    cin.getline(name, 20);
    scanf("%d %d %c %d %d", &en_r, &en_c, &en_dir, &out_r, &out_c);

    en_r1 = en_r + dr[dir_id(en_dir)];
    en_c1 = en_c + dc[dir_id(en_dir)];
    en_dir = dir_id(en_dir);

    if (strcmp(name, "END") == 0)return 0;
    while(scanf("%d",&r)){
        if (r == 0)break;
        scanf("%d", &c);
        while (scanf("%s", dt)) {
            if (dt[0] == '*')break;
            int len = strlen(dt);
            int did = dir_id(dt[0]);
            for (int i = 1; i < len; i++) 
                have_edge[r][c][did][turn_id(dt[i])] = 1;
        }
    }
    return 1;
}

这段代码虽繁琐,但没啥好讲的,就是根据输入创建图。

创建图之后就是运用BFS来寻找最短路径。BFS的模板伪代码如下:

void bfs(Node initialNode,Node endNode){
    queue<Node> q;
    q.push(initialNode);
    while(!q.empty()){
        Node x = q.pop();
        if(x == endNode){
            // 找到最短路径
            return;
        }
        for(Node y in 所有x能到达的且之前未访问过的合法结点){
            // 干点啥
            q.push(y);
        }
    }
}

这段代码的核心就是第二层的for,我们怎么去找所有x能到达的节点。很简单,我们不是用一个have_edge来表示图嘛,首先进到x节点的时候方向肯定已经确定了,我们要找的就是从这个方向进入x,都能往哪边转,题目中规定有直走,左转和右转,所以我们就得把这三种情况都试了,并找出能走的,放到队列中。

我们用一个d[r][c][dir]来记录从入口以dir方向到(r,c)所用的最短距离,其实此题中用不到,但是也需要一个这种东西来记录某个节点是否访问过,对于第一个节点,该距离为0。

void solve() {
    queue<NODE> q;
    memset(d, -1, sizeof(d));
    NODE u(en_r1, en_c1, en_dir);
    d[en_r1][en_c1][en_dir] = 0;
    q.push(u);
    while (!q.empty()) {
        u = q.front(); q.pop();
		if (u.r == out_r && u.c == out_c) {
			print_ans(u);
			return;
		}
        for (int i = 0; i < 3; i++) {
            NODE v = walk(u, i);
            if (have_edge[u.r][u.c][u.dir][i] && inside(v.r, v.c) && d[v.r][v.c][v.dir] < 0) {
                d[v.r][v.c][v.dir] = d[u.r][u.c][u.dir] + 1;
                p[v.r][v.c][v.dir] = u;
                q.push(v);
            }
        }
    }
    printf("No Solution Possible\n");
}

从上面的代码中,我们可以看到三个陌生的函数,print_ansinsidewalk,我们最关心的就是walk

walk(u,i)返回一个在节点ui方向走后得到的节点,该函数只管返回一个节点,而不会关心返回的节点在不在图里,以及i方向能不能走。所以在后面的if中还需要判断这个节点的合法性。

const int dr[] = { -1, 0, 1, 0 };
const int dc[] = { 0, 1, 0, -1 };
NODE walk(const NODE& u, int turn) {
    int dir = u.dir;
    if (turn == 1) 
        dir = (dir + 3) % 4;
    if (turn == 2)
        dir = (dir + 1) % 4;
    return NODE(u.r + dr[dir], u.c + dc[dir], dir);
}

这段代码不好理解,但挺好玩。这段代码如此简洁得益于我们给dirs独特的排列方式。dirs="NESW",恰好是从北面顺时针排列的,所以当我们从北面往左转的时候turn==1,使用dir=(dir+3)%4刚好得到3,代表的是西边。而如果从北边往右转时,得到的就是dir=(dir+1)%4,是东面。

dr和dc是一对偏移量数组,当往北去的时候,对于行来说就是减一,对于列来说就不变,就是这个意思。

所以walk方法能返回一个从节点uturn方向走后的位置。

inside函数更简单,只是用来判断给定的坐标在不在图内,这里不贴代码了,贴在后面。

for (int i = 0; i < 3; i++) {
    NODE v = walk(u, i);
    if (have_edge[u.r][u.c][u.dir][i] && inside(v.r, v.c) && d[v.r][v.c][v.dir] < 0) {
        d[v.r][v.c][v.dir] = d[u.r][u.c][u.dir] + 1;
        p[v.r][v.c][v.dir] = u;
        q.push(v);
    }
}

所以这段代码就是先往三个方向(直走,左转右转)走走看,如果路不通,或者跑到图外面去了,或者该节点已经从这个方向被访问过了,就放弃走这个节点,这是其实就是广度优先的基本套路。

然后如果发现这个节点能走,就走,并且把距离更新下,并且把新节点入队。

至于第二行p是干嘛的,它是用来记录父节点的,也不能叫父节点,就是假如我是从u走到v的,那么就记录p[v.r][v.c][v.dir] = u,用于一会儿输出答案。

那这代码就没啥了,大概就是这样,print_ans用于输出答案,也没啥好解释的,下面就是完整代码:

#include "iostream"
#include "cstdio"
#include "cstring"
#include "queue"
#define MAX 100

using namespace std;

struct NODE
{
    int r, c, dir;
    NODE(int r = 0, int c = 0, int dir = 0) : r(r), c(c), dir(dir) {}
};

const char* dirs = "NESW";
const char* turns = "FLR";
// have_edge[r][c][dir][turn] 表示在r行c列面朝dir时,能否向turn方向转
int have_edge[MAX][MAX][4][3];
char name[20];
int en_r, en_c, en_dir, out_r, out_c,en_r1,en_c1;
int d[MAX][MAX][4];
NODE p[MAX][MAX][4];
const int dr[] = { -1, 0, 1, 0 };
const int dc[] = { 0, 1, 0, -1 };

int dir_id(char c){
    return strchr(dirs, c) - dirs;
}

int turn_id(char c){
    return strchr(turns, c) - turns;
}

int scan() {
    int r, c;
    char dt[4];
    cin.getline(name, 20);
    scanf("%d %d %c %d %d", &en_r, &en_c, &en_dir, &out_r, &out_c);

    en_r1 = en_r + dr[dir_id(en_dir)];
    en_c1 = en_c + dc[dir_id(en_dir)];
    en_dir = dir_id(en_dir);

    if (strcmp(name, "END") == 0)return 0;
    while(scanf("%d",&r)){
        if (r == 0)break;
        scanf("%d", &c);
        while (scanf("%s", dt)) {
            if (dt[0] == '*')break;
            int len = strlen(dt);
            int did = dir_id(dt[0]);
            for (int i = 1; i < len; i++) 
                have_edge[r][c][did][turn_id(dt[i])] = 1;
        }
    }
    return 1;
}

NODE walk(const NODE& u, int turn) {
    int dir = u.dir;
    if (turn == 1) 
        dir = (dir + 3) % 4;
    if (turn == 2)
        dir = (dir + 1) % 4;
    return NODE(u.r + dr[dir], u.c + dc[dir], dir);
}
int inside(int r, int c) {
    return r > 0 && r <= 9 && c > 0 && c <= 9;
}
void print_ans(NODE u)
{
    vector<NODE> nodes;
    while (1) {
        nodes.push_back(u);
        if (d[u.r][u.c][u.dir] == 0) break;
        u = p[u.r][u.c][u.dir];
    }
    nodes.push_back(NODE(en_r, en_c, en_dir));

    //打印解,每行10个
    int cnt = 0;
    for (int i = (int)nodes.size() - 1; i >= 0; i--) {
        if (cnt % 10 == 0) printf(" ");
        printf(" (%d,%d)", nodes[i].r, nodes[i].c);//输出的时候不要带编码风格的空格,哭
        if (++cnt % 10 == 0) printf("\n");
    }
    if (nodes.size() % 10 != 0) printf("\n");
}
void solve() {
    queue<NODE> q;
    memset(d, -1, sizeof(d));
    NODE u(en_r1, en_c1, en_dir);
    d[en_r1][en_c1][en_dir] = 0;
    q.push(u);
    while (!q.empty()) {
        u = q.front(); q.pop();
		if (u.r == out_r && u.c == out_c) {
			print_ans(u);
			return;
		}
        for (int i = 0; i < 3; i++) {
            NODE v = walk(u, i);
            if (have_edge[u.r][u.c][u.dir][i] && inside(v.r, v.c) && d[v.r][v.c][v.dir] < 0) {
                d[v.r][v.c][v.dir] = d[u.r][u.c][u.dir] + 1;
                p[v.r][v.c][v.dir] = u;
                q.push(v);
            }
        }
    }
    printf("No Solution Possible\n");
}
int main() {
    while (scan()) {
        solve();
    }
    return 0;
}
posted @ 2020-11-08 13:44  yudoge  阅读(153)  评论(0编辑  收藏  举报