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,3
和0,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_ans
、inside
和walk
,我们最关心的就是walk
。
walk(u,i)
返回一个在节点u
向i
方向走后得到的节点,该函数只管返回一个节点,而不会关心返回的节点在不在图里,以及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
方法能返回一个从节点u
向turn
方向走后的位置。
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;
}