機器人搬重物(一道很經典的 BFS 題)

一道很經典的 BFS 題

想認真的寫篇題解。html

題目來自:https://www.luogu.org/problemnew/show/P1126ios

題目描述

機器人移動學會(RMI)如今正嘗試用機器人搬運物品。機器人的形狀是一個直徑$1.6米的球。在試驗階段,機器人被用於在一個儲藏室中搬運貨物。儲藏室是一個N×M的網格,有些格子爲不可移動的障礙。機器人的中心老是在格點上,固然,機器人必須在最短的時間內把物品搬運到指定的地方。機器人接受的指令有:向前移動1步(Creep);向前移動2步(Walk);向前移動3步(Run);向左轉(Left);向右轉(Right)。每一個指令所須要的時間爲1秒。請你計算一下機器人完成任務所需的最少時間。c++

輸入

第一行爲兩個正整數N,M(N,M≤50),下面N行是儲藏室的構造,0表示無障礙,1表示有障礙,數字之間用一個空格隔開。接着一行有4個整數和1個大寫字母,分別爲起始點和目標點左上角網格的行與列,起始時的面對方向(東E,南S,西W,北N),數與數,數與字母之間均用一個空格隔開。終點的面向方向是任意的。算法

輸出

一個整數,表示機器人完成任務所需的最少時間。若是沒法到達,輸出-1。數組

圖例

分析

之前在刷劉汝佳的紫書《算法競賽入門經典》時作過這道題,如今又再次遇到,幸不汝(辱)命,一次就過了。數據結構

BFS 性質

咱們都知道,BFS 具備從起點到目標節點(或狀態)路徑最短的特性,可是使用 BSF 這一特性時須要注意,只有當全部的邊權重相同(通常爲1)時,它才具備此性質,邊權不等時不具備。BFS 每次從一個節點只經歷一次轉移,當求單純的求距離時能夠認爲每次轉移的邊權爲一,轉移次數最少的路徑必定是距離最短的。咱們能夠用兩張圖來直觀的表現這個特性:測試

在圖上:this

初始節點爲 0,每次從一個節點向四周節點擴散,訪問全部距離爲一(相鄰,通過一次狀態轉換)的節點。spa

第一次擴散訪問了全部藍色節點,並無找到目標節點,繼續擴散。第二次擴散訪問了兩個粉色節點,虛線節點並無被訪問。由於在擴散到左邊的粉色節點時,咱們已經找到了目標節點。那麼不去搜索第三個粉色節點(虛線)節點不會丟解嗎?在圖中咱們也確實能看到,虛線節點在經歷一次擴散後,到達紫色節點,紫色節點再擴散一次,也可到達目標節點。不過,這個選擇不管是從上述圖片仍是從個人描述文字來看,他的距離都不短。那麼爲何會這樣呢?我說一下我我的的理解。看下面一幅圖:調試

在樹上:

在樹上的 BFS 被稱爲層次遍歷。他的工做原理就如其名,每次訪問一層的節點,同一層的節點有一個特色就是,他們的層數是相同的,也即到根節點的距離。當擴散到第三層時,(從左向右)第三個粉色節點做爲目標節點被發現,此時咱們能夠對比三種狀況:

  1. 因爲第三個粉色節點爲第一個目標節點,因此全部該節點左側同層節點都不是目標節點,而且從這些節點繼續擴散出的節點的層數(即到根節點的距離)必定 大於 第三個粉色節點到根節點的距離。
  2. 對於第三個粉色節點,也即咱們的第一個目標節點(爲何強調 「第一個」 由於可能有不止一個目標節點,即多個解),由他擴散而出的子節點的距離必定 大於 這個節點。
  3. 對於虛線節點,因爲他是否是目標節點,在未訪問到時未知,而他的性質是和第一種狀況類似的,因此去訪問並擴散虛線節點咱們能獲得的結果是他的距離 大於等於 第三個粉色節點

由此,能夠得出若是隻有一個解,那麼第一次被發現的目標節點 即第三個粉色節點必定是距離根節點最近的。

在圖上也能夠類比層的概念,得出相同的結論。

解題思路

BFS 的性質討論完,再來具體考慮這道題。這道題應該能明顯看出來,是在圖上尋找最短路的問題。不過首先須要對數據進行一些分析處理,才能更好的應用 BFS。此題惟一有些麻煩的就是,機器人具備半徑,將每一個格子單獨處理不方便。由給出的圖例能夠發現,機器人必定會佔據四個格子的空間,而一旦一個格子障礙物出現,那麼這個障礙物格子所在的四個四方格上的中心格點機器人都不能走(機器人只走格點)。

因此在讀完輸入鍵一個圖後,能夠再建立一個簡化版的圖,mini map。它將原圖中每四個格點當作一個點,而機器人只走這四個格子的中心,一旦一個四方格中有一個障礙,那麼這個四方格認爲不可走。這樣一來機器人移動、機器人半徑、單個方格障礙物問題就簡化成了最多見的狀況:在一個圖上的點避開障礙物到達另外一個點的最短路。(這中簡化和機器人在原圖上的移動是等價的,能夠模擬一下)。

如下是這部分處理代碼:

for(int i = 0; i < n; ++i) {
		for(int j = 0; j < m; ++j) {
			cin >> graph[i][j];
		}
	}

	int p = 0,q = 0;
	for(int i = 0; i < n-1; ++i) {
		q = 0;
		for(int j = 0; j < m-1; ++j) {
			if(graph[i][j] || graph[i][j+1] || graph[i+1][j] || graph[i+1][j+1])
				mini[p][q] = 1;//標記爲障礙,不然爲 0 表示可達
			q++;
		}
		p++;
	}

建出了圖,如今考慮狀態的轉移。此題中,每一個節點能夠有三種轉移狀況,向左轉,向右轉,移動。

每種狀況耗時爲 1(能夠認爲是距離,權重)。關於轉向問題,能夠定義一個方向數組,按順時針順序給出北東南西,而後不管朝向那個方位,向左轉就是數組索引減一,向右轉就是加一,檢索方向數組得到新的方位。完成一次轉向後,就完成了一次狀態轉移,將新的節點入隊列,這個新的節點在下一次被取出考慮進行轉移狀態時,他的當前方向就是移動的方向。

宏和數據結構:

#define MAX 50
#define DIR 4
#define N 0
#define E 1
#define S 2
#define W 3

struct Node {
	int x,y;
	int step = 0;
	int d;
	Node (int x,int y,int step,int dir) {
		this->x = x,this->y = y,this->step = step,this->d = dir;
	}
	// Node () {}
	// int pre = 0;
	// int loc = 0;
};

int graph[MAX + 1][MAX + 1];
int mini[MAX + 1][MAX + 1];		//mini graph
int vis[MAX + 1][MAX + 1][DIR];
//			N  E  S  W
int dx[] = {-1,0, 1, 0};
int dy[] = {0, 1, 0,-1};
int n,m;

BFS 代碼:

Node bfs(int sx,int sy,int tx,int ty,int dir) {
	queue<Node> q;
	Node start = Node(sx,sy,0,dir);
	q.push(start);
	// path[pi++] = start;
	vis[sx][sy][dir] = 1;
	while(!q.empty()) {
		Node u = q.front();
		q.pop();

		if(u.x == tx && u.y == ty) return u;

		//嘗試向左轉
		if(!vis[u.x][u.y][(u.d-1+DIR)%DIR]) {
			vis[u.x][u.y][(u.d-1+DIR)%DIR] = 1;
			Node nu = Node(u.x,u.y,u.step+1,(u.d-1+DIR)%DIR);
			// nu.pre = u.loc;		//記錄路徑信息
			// nu.loc = pi;
			// path[pi++] = nu;
			q.push(nu);
		}
		//嘗試向右轉
		if(!vis[u.x][u.y][(u.d+1)%DIR]) {
			vis[u.x][u.y][(u.d+1)%DIR] = 1;
			Node nu = Node(u.x,u.y,u.step+1,(u.d+1)%DIR);
			// nu.pre = u.loc;
			// nu.loc = pi;
			// path[pi++] = nu;
			q.push(nu);
		}
		//嘗試移動 creep,walk,run
		for(int i = 1; i <= 3; ++i) {
			Node nu = Node(u.x,u.y,u.step+1,u.d);
			//判斷是是否有障礙物。只要有一個,就沒必要移動了。
			bool isok = true;
			for(int j = 0; j < i; ++j) {
				if(mini[nu.x+dx[u.d]*j][nu.y+dy[u.d]*j])
					{ isok = false; break; }
			}
			if(isok == false) break;

			nu.x += dx[u.d] * i;
			nu.y += dy[u.d] * i;

			if(inrange(nu) && !vis[nu.x][nu.y][nu.d]) {
				vis[nu.x][nu.y][nu.d] = 1;
				// nu.pre = u.loc;
				// nu.loc = pi;
				// path[pi++] = nu;
				q.push(nu);
			}
		}
	}
	return Node(-1,-1,-1,-1);
}

其中註釋代碼是用來記錄最短路的路徑信息。在這裏是我用來測試程序的。

在調試的時候遇到一個 bug 花了我幾個小時,機器人移動方式有 creep,walk,run,分別移動 1 、2 、3 步,在移動步數大於 1 時,不能只判斷目標節點是不是障礙物,而要判斷每一步是否有障礙物。

完整程序:

#include <iostream>
#include <algorithm>
#include <cstring>
#include <stdlib.h>
#include <memory.h>
#include <queue>
using namespace std;

#define MAX 50
#define DIR 4
#define N 0
#define E 1
#define S 2
#define W 3

struct Node {
	int x,y;
	int step = 0;
	int d;
	Node (int x,int y,int step,int dir) {
		this->x = x,this->y = y,this->step = step,this->d = dir;
	}
	// Node () {}
	// int pre = 0;
	// int loc = 0;
};

int graph[MAX + 1][MAX + 1];
int mini[MAX + 1][MAX + 1];		//mini graph
int vis[MAX + 1][MAX + 1][DIR];
//			N  E  S  W
int dx[] = {-1,0, 1, 0};
int dy[] = {0, 1, 0,-1};
int n,m;

// Node path[MAX * MAX + 1];
// int pi = 0;

bool inrange(Node nd) {
	return nd.x >= 0 && nd.x <= n-2 && nd.y >= 0 && nd.y <= m-2;
}

Node bfs(int sx,int sy,int tx,int ty,int dir) {
	queue<Node> q;
	Node start = Node(sx,sy,0,dir);
	q.push(start);
	// path[pi++] = start;
	vis[sx][sy][dir] = 1;
	while(!q.empty()) {
		Node u = q.front();
		q.pop();

		if(u.x == tx && u.y == ty) return u;

		//嘗試向左轉
		if(!vis[u.x][u.y][(u.d-1+DIR)%DIR]) {
			vis[u.x][u.y][(u.d-1+DIR)%DIR] = 1;
			Node nu = Node(u.x,u.y,u.step+1,(u.d-1+DIR)%DIR);
			// nu.pre = u.loc;		//記錄路徑信息
			// nu.loc = pi;
			// path[pi++] = nu;
			q.push(nu);
		}
		//嘗試向右轉
		if(!vis[u.x][u.y][(u.d+1)%DIR]) {
			vis[u.x][u.y][(u.d+1)%DIR] = 1;
			Node nu = Node(u.x,u.y,u.step+1,(u.d+1)%DIR);
			// nu.pre = u.loc;
			// nu.loc = pi;
			// path[pi++] = nu;
			q.push(nu);
		}
		//嘗試移動 creep,walk,run
		for(int i = 1; i <= 3; ++i) {
			Node nu = Node(u.x,u.y,u.step+1,u.d);
			////判斷是是否有障礙物。只要有一個,就沒必要移動了。
			bool isok = true;
			for(int j = 1; j <= i; ++j) {
				if(mini[nu.x+dx[u.d]*j][nu.y+dy[u.d]*j])
					{ isok = false; break; }
			}
			if(isok == false) break;

			nu.x += dx[u.d] * i;
			nu.y += dy[u.d] * i;

			if(inrange(nu) && !vis[nu.x][nu.y][nu.d]) {
				vis[nu.x][nu.y][nu.d] = 1;
				// nu.pre = u.loc;
				// nu.loc = pi;
				// path[pi++] = nu;
				q.push(nu);
			}
		}
	}
	return Node(-1,-1,-1,-1);
}

int main(int argc, char const *argv[])
{
	freopen("/home/skipper/Documents/code/刷題/洛谷 OJ/重啓/in.txt","r",stdin);
	int sx,sy,tx,ty;
	char dir;
	cin >> n >> m;
	for(int i = 0; i < n; ++i) {
		for(int j = 0; j < m; ++j) {
			cin >> graph[i][j];
		}
	}

	int p = 0,q = 0;
	for(int i = 0; i < n-1; ++i) {
		q = 0;
		for(int j = 0; j < m-1; ++j) {
			if(graph[i][j] || graph[i][j+1] || graph[i+1][j] || graph[i+1][j+1])
				mini[p][q] = 1;
			q++;
		}
		p++;
	}
	int d;
	cin >> sx >> sy >> tx >> ty >> dir;
	switch(dir) {
		case 'S': d = S;break;
		case 'N': d = N;break;
		case 'E': d = E;break;
		case 'W': d = W;break;
	};

	//test 查看 mini map
	// for(int i = 0; i < p; ++i) {
	// 	for(int j = 0; j < q; ++j) {
	// 		cout << mini[i][j] << " ";
	// 	}
	// 	cout << endl;
	// }

	Node res = bfs(sx-1,sy-1,tx-1,ty-1,d);

	//輸出路徑
	// int pp = res.loc;
	// do {
	// 	cout << path[pp].x << "," << path[pp].y << endl;
	// 	pp = path[pp].pre;
	// }while(pp);

	cout << res.step << endl;
	return 0;
}

若有錯誤,歡迎指正評論。

<br><br> 做者:<a href='https://www.cnblogs.com/backwords/'>Skipper</a> 出處:https://www.cnblogs.com/backwords/p/10542486.html 本博客中未標明轉載的文章歸做者 <a href='https://www.cnblogs.com/backwords/'>Skipper</a> 和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。

相關文章
相關標籤/搜索