可用BFS求解图中两个结点之间的最短路径。这样的图通常在形式上为矩形点阵(网格迷宫),每个可走的点(网格,下同)为图的结点,图的边则描述了从一个结点与其相邻结点直接连通的状态。在二叉树的BFS中,结点的访问顺序恰好是它们到根结点距离从小到大的顺序。类似地,图的BFS的过程就是把从起点到终点过程中遍历的点建成以起点为根结点,终点为叶节点的一棵树(称为最短路树,或者BFS树)的过程。遍历的顺序为到起点的距离。规定同一个结点只能被拓展到一次,从而保证父结点的唯一性和拓展路径最短。
【提示】很多复杂的迷宫问题都可以转化为最短路问题,然后用BFS求解。在套用BFS框架之前,需要先搞清楚图中的“结点”包含哪些内容。
【题目】
有一个最多包含9×9个交叉点的迷宫。输入起点、离开起点时的朝向和终点,求一条最短路(多解时任意输出一个即可)。
这个迷宫的特殊之处在于:进入一个交叉点的方向不同,允许出去的方向也不同。
注意:初始状态是“刚刚离开入口”,所以即使出口和入口重合,最短路也不为空。
【分析】
本题和普通的迷宫在本质上是一样的,但是由于“朝向”也起到了关键作用,所以需要用一个三元组(r, c, dir)表示“位于(r,c),面朝dir”这个状态。由于起点没有路牌,且指定了到下一个点唯一路径与朝向,假设入口位置为(r0, c0),朝向为dir,则初始状态并不是(r0, c0, dir),而是(r1, c1, dir),其中,(r1,c1)是(r0,c0)沿着方向dir走一步之后的坐标。
首先是输入过程。将4个方向和3种“转弯方式”编号为0~3和0~2,并且提供相应的转换函数。输入函数比较简单,作用就是读取r0, c0, dir,并且计算出r1, c1,然后读入has_edge数组。
接下来是“行走”函数,根据当前状态和转弯方式,计算出后继状态。
下面是BFS主过程。
最后是解的打印过程。它也可以写成递归函数,不过用vector保存结点可以避免递归时出现栈溢出,并且更加灵活。
【提示】使用BFS求出图的最短路之后,可以用递归方式打印最短路的具体路径。如果最短路非常长,递归可能会引起栈溢出,此时可以改用循环,用vector保存路径。
#include
#include
#include
#include
using namespace std;
struct Node {
int r, c, dir; // 站在(r,c),面朝方向dir
Node(int r=0, int c=0, int dir=0):r(r),c(c),dir(dir) {}
//!结构体的递归定义
};
const int maxn = 10;
const char* dirs = "NESW"; // 顺时针旋转
const char* turns = "FLR";
int has_edge[maxn][maxn][4][3];
int d[maxn][maxn][4];
Node p[maxn][maxn][4];
int r0, c0, dir, r1, c1, r2, c2;
int dir_id(char c) { return strchr(dirs, c) - dirs; }
//将代表朝向的4种字符转换成相应的编号
int turn_id(char c) { return strchr(turns, c) - turns; }
//将代表转向的3种字符转换成相应的编号
const int dr[] = {-1, 0, 1, 0};//4种走向对应的4种行坐标变化
const int dc[] = {0, 1, 0, -1};//4种走向对应的4种列坐标变化
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);
}
bool inside(int r, int c) //判断当前坐标是否越界
{return r >= 1 && r <= 9 && c >= 1 && c <= 9;}
bool read_case() {
char s[99], s2[99];
if(scanf("%s%d%d%s%d%d", s, &r0, &c0, s2, &r2, &c2) != 6)
return false;
printf("%s\n", s);
dir = dir_id(s2[0]);
r1 = r0 + dr[dir];
c1 = c0 + dc[dir];
memset(has_edge, 0, sizeof(has_edge));
for(;;){
int r, c;
scanf("%d", &r);//r代表行
if(r == 0) break;
scanf("%d", &c);//c代表列
while(scanf("%s", s) == 1 && s[0] != '*'){
for(int i = 1; i < strlen(s); i++)
has_edge[r][c][dir_id(s[0])][turn_id(s[i])] = 1;
//has_edge[r][c][dir][turn]表示当前状态是(r,c,dir),是否可以沿着转弯方向turn行走。
}
}
return true;
}
void print_ans(Node u){
// 从目标结点逆序追溯到初始结点
vector<Node> nodes;
for(;;){
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(r0, c0, dir));
// 打印解,每行10个
int cnt = 0;
for(int i = 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));//-1表示起点到该点的最短路尚未生成
Node u(r1, c1, dir);//相当于"Node u={r1,c1,dir};"
d[u.r][u.c][u.dir] = 0;//d[r][c][dir]表示初始状态到(r,c,dir)的最短路长度
q.push(u);
while(!q.empty()) {
Node u = q.front(); q.pop();
if(u.r == r2 && u.c == c2) { print_ans(u); return; }
for(int i = 0; i < 3; i++){ //尝试向4个方向拓展
Node v = walk(u, i);
if(has_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;
//用p[r][c][dir]保存状态(r,c,dir)在BFS树中的父结点
q.push(v);
}
}
}
printf(" No Solution Possible\n");
}
int main() {
while(read_case())
solve();
return 0;
}
【题目】
时间限制: 3 Sec 内存限制: 128 MB
题目描述
一个机器人从m×n的网格的左上角(1,1)走到右下角(m,n),每个格子是空地或者障碍,机器人每次可以向四个方向走一步,但是不能连续踩上k个障碍,求最短步数。(起点和终点都是空地)
输入
输入有多组。输入文件的第一行为组数,不大于20。 每组输入的第一行包含两个用空格分隔的正整数m和n (1≤m,n≤20),第二行包含一个整数k(0≤k≤20)。接下来m行,每行包含用空格分隔的n个整数,以值“1”代表障碍,“0”代表空地。(i = 1, 2, . . . , m; j = 1, 2, . . . , n)
输出
对于每组输入,如果机器人可以到达(m,n),则输出机器人需要移动的最少次数,否则输出“-1”。
样例输入
3
2 5
0
0 1 0 0 0
0 0 0 1 0
4 6
1
0 1 1 0 0 0
0 0 1 0 1 1
0 1 1 1 1 0
0 1 1 1 0 0
2 2
0
0 1
1 0
样例输出
7
10
-1
【分析】
变在哪里:
普通BFS的结点只有两种情况,通或者不通,能走的路是通路,一个子节点只能属于一个父节点,因此必然存在一个树可以连通所有通路结点,从而求出最短路径。
而本题可以跨越障碍,这样就产生了至少有三种状态:通,有障碍但是可以走(即在k范围内),不通。从树的角度来看,一个子节点可能属于多个父节点。(也就是说,走已经走过的点也可能是最优解,普通BFS走已经走过的点必然不是最优解)因此,不能单纯的连成一个二叉树,而是要连成一个图,在图中找最短路。
在寻路过程中,每一步都受到过去的影响,而且影响未来。如果当前遇到障碍,过去已跨越的障碍数量决定了是否能够跨越当前障碍;当前是否跨过障碍,决定了未来能否跨过障碍。显然,走到当前位置跨越的障碍越少越好,因为之后可能还有障碍,这样就可以跨过更多的障碍。
解题关键:
在允许重复访问结点的同时,避免走回头路,仅保留跨越障碍最少的方案。
解决方法:
取消对结点“已访问”状态的标记,用一个数组存储到达当前结点最少需要跨越几个障碍。(由于BFS的特性,到同一个结点的步数必然是相同的,但是它跨过的障碍数可能不同)。走到一个点后,查表判断其是否更优,若不是,则舍弃。为什么这样可行?如果试图访问已走过的结点,再次访问时跨越障碍的数量一定不会比第一次少,会被舍弃;由于试探有顺序,可能在到达最优之前保存了不完美的方案,但最少障碍会更新,不完美方案最终会被淘汰。
可行方案倒推:结点的状态增加了连续踩上障碍的数量,对于每个网格(不管有无障碍)还是只允许访问一次,因为对于一个目的网格其周围有多个相邻网格,走到这些相邻网格时连续踩上障碍的数量也不同,只要解存在,总可以产生,所以可行。
#include
#include
#include
using namespace std;
const int N=20+5;
const int inf=N*N;
struct node
{int r,c,o;};//r为行标,c为列标,o为连续踩上障碍数
int m,n,k,map[N][N];
void read_case(){
cin>>m>>n;//m为行数,n为列数
cin>>k;//k为连续踩上障碍上限
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
cin>>map[i][j];
}
int d[N][N][N];
const int dr[] = {1, 0, -1, 0};
const int dc[] = {0, 1, 0, -1};
bool inside(int r, int c)
{return r>=1&&r<=m&&c>=1&&c<=n;}
node walk(const node& u, int i){
int r=u.r+dr[i],c=u.c+dc[i],o=u.o;
if(inside(r,c)){
if(map[r][c]) o++;else o=0;
return {r,c,o};
}
else return {0,0,inf};
}
void solve(){
queue<node>q;
memset(d, -1, sizeof(d));//-1表示起点到该点的最短路尚未生成
d[1][1][0] = 0;//d[r][c][o]表示初始状态到(r,c,o)的最短路长度
node u={1,1,0};q.push(u);
int ans=-1;
while(!q.empty()){
node u=q.front();q.pop();
if(u.r==m&&u.c==n) {ans=d[m][n][u.o];break;}
for(int i=0;i<4;i++){
node v=walk(u,i);
if(v.o<=k&&d[v.r][v.c][v.o]<0){
d[v.r][v.c][v.o]=d[u.r][u.c][u.o]+1;
q.push(v);
}
}
}
cout<<ans<<endl;
}
int main(){
int ca;cin>>ca;
while(ca--)
{read_case();solve();}
return 0;
}
【题目】
有一个奇怪的大楼和奇怪的电梯,大楼每一层的地上都标着一个数值。电梯一次只能移动(上升或下降)指定层数,就是移动前所在楼层地上的数值。输入起始楼层A、楼层数N和目的楼层B,输出最少移动次数。(电梯最高可到达N层,最低可到达1层)(如果无解,输出-1)
【思路】
广度优先搜索思考如下问题:
队列存储内容?
每次扩展方向及数值?
越界判断?
流程:
每个结点步数初始化为无穷大
起点加入队首,步数设为0
循环处理队列:取队首,终点判断,向各个方向扩展,可扩展者入队并更新步数,队首出队
判断终点步数是否被缩短过
#include
#include
using namespace std;
int main(){
int n;
while(cin>>n&&n){
int A,B,i,fr;
int k[205]={0};//k存储各楼层对应的上下值
int s[205]={0};//s存储走到该楼层所用步数
cin>>A>>B;
for(i=1;i<=n;i++)
{cin>>k[i];s[i]=205;}//s初始化为无穷大
queue<int>q;//队列存储走到的楼层
q.push(A);s[A]=0;//从A出发,入队
while(!q.empty()){//队列非空
fr=q.front();//取队首存入fr
if(fr==B) break;//到达终点
if(fr+k[fr]<=n&&s[fr+k[fr]]>s[fr]+1)//fr+k[fr]未越界且未走到过
{q.push(fr+k[fr]);s[fr+k[fr]]=s[fr]+1;}//向上扩展新结点
if(fr-k[fr]>=1&&s[fr-k[fr]]>s[fr]+1)//向下扩展新结点
{q.push(fr-k[fr]);s[fr-k[fr]]=s[fr]+1;}
q.pop();//队首出列
}
if(s[B]!=205) cout<<s[B]<<endl;
else cout<<"-1\n";
}
return 0;
}
【题目】
你困在一个3D地牢中,每个单元可走(普通.
,起点S
,终点E
)或不可走(#
),只能向东南西北上下移动。走一格要花一分钟,分层输入地牢地图,输出最短逃逸时间。
#include
#include
using namespace std;
struct node
{int z;int x;int y;int t;}fr;//z代表层数,t代表步数
queue<node>q;
char map[33][33][33];int l,r,c,i,j,k;
void finds()
{
for(i=0;i<l;i++)
for(j=0;j<r;j++)
for(k=0;k<c;k++)
if(map[i][j][k]=='S')
{q.push({i,j,k,0});map[i][j][k]='#';return;}
}
int dir[6][3]={{0,1,0},{0,-1,0},{1,0,0},{-1,0,0},{0,0,1},{0,0,-1}};//方向数组
int finde(){
int nx,ny,nz,mt;
while(!q.empty()){
fr=q.front();
for(i=0;i<6;i++){
nx=fr.x+dir[i][0];ny=fr.y+dir[i][1];nz=fr.z+dir[i][2];
if(nx<0||nx==r||ny<0||ny==c||nz<0||nz==l) continue;
switch(map[nz][nx][ny])//对下一结点进行处理
{
case '#':break;
case '.':q.push({nz,nx,ny,fr.t+1});map[nz][nx][ny]='#';break;
case 'E':return fr.t+1;
}
}
q.pop();
}
return 0;
}
int main()
{
while(scanf("%d%d%d",&l,&r,&c)&&l&&r&&c){
for(i=0;i<l;i++){
getchar();
for(j=0;j<r;j++)
gets(map[i][j]);
}
getchar();
while(!q.empty()) q.pop();
finds();
int ans=finde();
if(ans>0) printf("Escaped in %d minute(s).\n",ans);
else printf("Trapped!\n");
}
return 0;
}
【题目】
公主被关在一个两层的迷宫里,骑士需要在T时刻或之前找到公主。迷宫的入口是S(0,0,0),公主的位置用P表示,时空传输机用#表示,墙用*
表示,平地用.
表示。骑士们一进入时空传输机就会被转到另一层的相对位置,但如果被转到的位置是墙的话,那骑士们就会被撞死。骑士们在一层中只能上下左右移动,每移动一格花1时刻。层间的移动只能通过时空传输机,且不需要任何时间。输入地图,判断骑士能否完成任务。
【思路】
地图的两层之间有联系,故用三维数组存储。
多分支选择处理迷宫格的值,遇到时空传输机则需嵌套一个多分支选择处理另一层迷宫格的值。
访问过的点标记为墙。
时空传输机对应墙的标为墙(所有层)。两个时空传输机相对会死循环,也标为墙。
#include
#include
using namespace std;
struct node
{int x;int y;int z;int t;}fr,temp;//z代表层数,t代表步数
int cs(int z)
{if(z==0) return 1;
return 0;}
int main(){
int c;
char map[2][15][15];//三维数组
int dir[4][2]={0,1,0,-1,1,0,-1,0};//方向数组
scanf("%d",&c);
while(c--){
int n,m,T,i;
scanf("%d%d%d",&n,&m,&T);
getchar();
for(i=0;i<n;i++) gets(map[0][i]);
getchar();//消化空行
for(i=0;i<n;i++) gets(map[1][i]);
queue<node>q;
temp.x=temp.y=temp.z=temp.t=0;q.push(temp);
int find=0,nx,ny,nz,mt;
while(!q.empty()){
fr=q.front();
for(i=0;i<4;i++){
nx=fr.x+dir[i][0];ny=fr.y+dir[i][1];nz=fr.z;
if(nx<0||nx==n||ny<0||ny==m) continue;
switch(map[nz][nx][ny]){//对下一结点进行处理
case '#':switch(map[cs(nz)][nx][ny])
{case '*':case '#'://若传输到墙或传输机,将两处都处理为墙
map[0][nx][ny]='*';map[1][nx][ny]='*';break;
case '.':map[nz][nx][ny]='*';
temp.x=nx;temp.y=ny;temp.z=cs(nz);temp.t=fr.t+1;q.push(temp);break;
case 'P':mt=fr.t+1;find=1;break;}
case '*':break;
case '.':temp.x=nx;temp.y=ny;temp.z=nz;temp.t=fr.t+1;q.push(temp);break;
case 'P':mt=fr.t+1;find=1;break;
}
if(find==1) break;
}
if(find==1) break;
map[fr.z][fr.x][fr.y]='*';//队首标记为墙,表示已访问过
q.pop();
}
if(find==1&&mt<=T) printf("YES\n");
else printf("NO\n");
}
return 0;
}
【题目】
有一个n行m列的迷宫,每个网格要么是草地,要么是空地。选择2块草地(可以选同一块)点火并开始计时,火会向相邻的草地(东南西北)传播,速度为每秒一格。如果下一秒没有新的草地着火,则计时在当前时刻结束。如果计时结束后所有的草地都已着火,则视为有解,解的值为计时结束时刻。
输入迷宫地图,求最小解。
【思路】
分别在2个格子同时点火
(1)2路火可能会相遇
2路火不一定同时到达同一个格子,除了相遇点。每个格子在着火之后即标记已访问。
(2)是否烧完
遍历所有#
,看是否都被访问过。
【注】不考虑起点相同,则答案错误。1个起点所花时间竟然比2个起点少,无法理解。
#include
#include
#include
const int N=15;
const int inf=N*N;
using namespace std;
int n,m,r1,r2,c1,c2;//n为行数,m为列数
char g[N][N];bool vis[N][N];
struct node
{int r,c,d;}tnode;//r为行标,c为列标,d为最短路长度
const int dr[] = {1, 0, -1, 0};
const int dc[] = {0, 1, 0, -1};
bool inside(int r, int c)
{return r>=0&&r<n&&c>=0&&c<m;}
int bfs(){//返回这2个点火点的最短时间
memset(vis,true,sizeof(vis));
vis[r1][c1]=false;vis[r2][c2]=false;
int ans=-1;
queue<node>q;
tnode.r=r1;tnode.c=c1;tnode.d=0;q.push(tnode);
tnode.r=r2;tnode.c=c2;tnode.d=0;q.push(tnode);
while(!q.empty()){
node u=q.front();q.pop();
if(u.d>ans) ans=u.d;
for(int i=0;i<4;i++){
int vr=u.r+dr[i],vc=u.c+dc[i];
if(inside(vr,vc)&&g[vr][vc]=='#'&&vis[vr][vc])
{tnode.r=vr;tnode.c=vc;tnode.d=u.d+1;q.push(tnode);
vis[vr][vc]=false;}
}
}
return ans;
}
bool check(){
for(int r=0;r<n;r++)
for(int c=0;c<m;c++)
if(g[r][c]=='#'&&vis[r][c])
return false;
return true;
}
int solve(){//返回每组最短等待时间
int time=inf;
for(r1=0;r1<n;r1++)//每个点火点1
for(c1=0;c1<m;c1++)
if(g[r1][c1]=='#'){//可以点火
int first=1;
//第一个尝试的点火点2列标与点火点1相同,之后每行都是从头开始
for(r2=r1;r2<n;r2++)//每个点火点2
for(c2=0;c2<m;c2++){
if(first) {first=0;if(r2==r1) c2=c1+1;}
if(g[r2][c2]=='#'){//可以点火
int temp=bfs();
if(check()&&temp<time) time=temp;
}
}
}
if(time<inf) return time;
return -1;
}
int main(){
int T,t,i;
scanf("%d",&T);
for(t=1;t<=T;t++){
scanf("%d%d",&n,&m);getchar();
for(i=0;i<n;i++) gets(g[i]);
printf("Case %d: %d\n",t,solve());
}
return 0;
}