用BFS求最短路 - 习题6道

可用BFS求解图中两个结点之间的最短路径。这样的图通常在形式上为矩形点阵(网格迷宫),每个可走的点(网格,下同)为图的结点,图的边则描述了从一个结点与其相邻结点直接连通的状态。在二叉树的BFS中,结点的访问顺序恰好是它们到根结点距离从小到大的顺序。类似地,图的BFS的过程就是把从起点到终点过程中遍历的点建成以起点为根结点,终点为叶节点的一棵树(称为最短路树,或者BFS树)的过程。遍历的顺序为到起点的距离。规定同一个结点只能被拓展到一次,从而保证父结点的唯一性和拓展路径最短。

【提示】很多复杂的迷宫问题都可以转化为最短路问题,然后用BFS求解。在套用BFS框架之前,需要先搞清楚图中的“结点”包含哪些内容。

Abbott的复仇(ACM/ICPC World Finals 2000)

【题目】
有一个最多包含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<cstdio>
#include<cstring>
#include<vector>
#include<queue>
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;
}

巡逻机器人(ACM/ICPC Hanoi 2006)

【题目】
时间限制: 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<iostream>
#include<cstring>
#include<queue>
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;
}

HDOJ-1548 A strange lift

【题目】
有一个奇怪的大楼和奇怪的电梯,大楼每一层的地上都标着一个数值。电梯一次只能移动(上升或下降)指定层数,就是移动前所在楼层地上的数值。输入起始楼层A、楼层数N和目的楼层B,输出最少移动次数。(电梯最高可到达N层,最低可到达1层)(如果无解,输出-1)

【思路】

广度优先搜索思考如下问题:
队列存储内容?
每次扩展方向及数值?
越界判断?

流程:
每个结点步数初始化为无穷大
起点加入队首,步数设为0
循环处理队列:取队首,终点判断,向各个方向扩展,可扩展者入队并更新步数,队首出队
判断终点步数是否被缩短过

#include<iostream>
#include<queue>
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;
}

POJ-2251 Dungeon Master

【题目】

你困在一个3D地牢中,每个单元可走(普通.,起点S,终点E)或不可走(#),只能向东南西北上下移动。走一格要花一分钟,分层输入地牢地图,输出最短逃逸时间。

#include<cstdio>
#include<queue>
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;
}

HDOJ-2102 A计划

【题目】
公主被关在一个两层的迷宫里,骑士需要在T时刻或之前找到公主。迷宫的入口是S(0,0,0),公主的位置用P表示,时空传输机用#表示,墙用*表示,平地用.表示。骑士们一进入时空传输机就会被转到另一层的相对位置,但如果被转到的位置是墙的话,那骑士们就会被撞死。骑士们在一层中只能上下左右移动,每移动一格花1时刻。层间的移动只能通过时空传输机,且不需要任何时间。输入地图,判断骑士能否完成任务。

【思路】
地图的两层之间有联系,故用三维数组存储。
多分支选择处理迷宫格的值,遇到时空传输机则需嵌套一个多分支选择处理另一层迷宫格的值。
访问过的点标记为墙。
时空传输机对应墙的标为墙(所有层)。两个时空传输机相对会死循环,也标为墙。

#include<cstdio>
#include<queue>
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;
}

FZU-2150 Fire Game

【题目】
有一个n行m列的迷宫,每个网格要么是草地,要么是空地。选择2块草地(可以选同一块)点火并开始计时,火会向相邻的草地(东南西北)传播,速度为每秒一格。如果下一秒没有新的草地着火,则计时在当前时刻结束。如果计时结束后所有的草地都已着火,则视为有解,解的值为计时结束时刻。
输入迷宫地图,求最小解。

【思路】
分别在2个格子同时点火
(1)2路火可能会相遇
2路火不一定同时到达同一个格子,除了相遇点。每个格子在着火之后即标记已访问。
(2)是否烧完
遍历所有#,看是否都被访问过。
【注】不考虑起点相同,则答案错误。1个起点所花时间竟然比2个起点少,无法理解。

#include<cstdio>
#include<cstring>
#include<queue>
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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值