談談面試--迷宮尋路系列


 

前言:
  又到了人才流動的高峰季節, "金三銀四", 過了這個村, 就沒那個店, ^_^. 面試者勤奮地准備題典, 面試官也在奮筆疾書, ^_^.
  有些面試官喜歡廣度的知識覆蓋, 而有些面試官喜歡深度的知識探求.
  筆者不是面試者, 也不面試官, 但想結合自身的學習和工作經歷, 對深度型的題材做下嘗試和研究.
  這篇讓我們談談迷宮尋路系列, 基礎篇, 進階篇難度篇.

基礎篇:
  讓我們先來構造一個游戲場景:
  在一個迷宮中, 鼠精靈需要繞過巨石, 找到迷宮中的出口, 求最短路徑?
  
  關於該問題, 大家肯定不假思索的提到: 寬度優先遍歷(BFS).

	// 1).初始化工作
	//    1.1). 把源節點坐標放入隊列中
	queue.push((x, y,step=0));
	//    1.2). 標示該節點訪問過
	visited[(x, y)] = true
	
	// 2).BFS procedure
	while ( !queue.empty() ) {
		// 2.1).取出當前節點
		(x, y, step) <= queue.pop()
		// 2.2).判斷是否為目標節點, 並返回
		if ( (x, y) == (dest.x, dest.y) ) {
			return (step);
		}
		// 2.3).遍歷(x, y)的鄰近節點
		foreach ((x', y') in neighbor(x, y)) {
			// 2.3.1).可到達且沒有訪問過
			if (is_available(x', y') and visited[(x, y)] == false) {
				queue.push((x', y', step + 1));
				// 標示訪問過
				visited[(x', y')] = true
			}			
		}		
	}

	// 3). 不存在路徑
	return unavailable

  注: 判斷是否到達目標節點的代碼片段比較靈活, 為了加速可放到2.3.1) IF判斷里面.
  確實很簡單, 不過這是最基本的.

進階篇:
  同樣的場景, 如果迷宮很大, 那使用BFS的話, 效果就不是很高. 那是否存在更高效的算法呢?
  有兩種成熟而常規的實現思路: A*算法雙向寬度優先搜索.
  1). A*算法:
  該算法引入啟發式評估函數, 用以加速最短路徑求解過程.
  核心概念:
  • 歷史代價g(n): 從初始節點到n節點的實際代價, 代表過去和現在
  • 預測評估h(n): 當前節點n到目標節點的預測代價, 代表未來
  • 啟發評估f(n): 節點n的估價函數, 其滿足f(n) = g(n) + h(n)
  這邊特別要注意的一個先決條件: 預測函數h(n) <= 實際的真實代價
  預測函數h(n)越接近於真實代價, 其啟發評估的效果越好.
  更詳細的請參考如下博文"Amit's A star Page中譯文". 
  這邊我們選擇曼哈頓距離作為預測函數h(n), 整體的框架代碼如下:

	// 1).初始化工作
	//    1.1). 把源節點坐標放入優先隊列中
	priority_queue.push((x, y, f(x,y)=0));
	//    1.2). 標示(x, y)的代價為0, 其余皆為無限大
	cost[(x, y)] = (f(x, y) = 0)
	
	// 2).BFS procedure
	while ( !priority_queue.empty() ) {
		// 2.1).取出當前節點
		(x, y, f(x,y)=step) <= priority_queue.pop()
		//	過濾掉中間節點
		if ( f(x,y) > cost[(x, y)] ) {
			continue;
		}		

		// 2.2).判斷是否為目標節點, 並返回
		if ( (x, y) == (dest.x, dest.y) ) {
			return f(x, y);
		}
		// 2.3).遍歷(x, y)的鄰近節點
		foreach ((x', y') in neighbor(x, y)) {
			// 可到達且節點有更優的解
			g(x',y') = f[(x, y)] + 1;	
			if ( is_available(x', y') and (cost[(x', y')] > g(x',y') + h(x', y')) ) {
				priority_queue.push((x', y', f(x',y')=g(x',y')+h(x',y')));
				// 更新該節點的評估值
				cost[(x', y')] = f(x',y')=g(x',y')+h(x',y')
			}			
		}		
	}

	// 3). 不存在路徑
	return unavailable

  注: 於BFS版相比, 這邊使用優先隊列代替FIFO的隊列, 並借助代價cost表代替訪問visited表.

  2). 雙向寬度優先遍歷:
  該算法借助起點和終點的雙向寬度遍歷, 來加速最短路徑的求解過程.
  算法的流程和代碼就不再具體展開了, 讓我們通過畫圖來形象地比較各個算法的優劣.

  尋路算法遍歷的節點數量, 可用面積來表示. 圖中可得BFS是圓型, A*是橢球型, 雙向寬度搜索則是兩個剛好相切的圓形. 從圖形面積對比中, 我們可以獲取到各個算法優劣的視覺直觀體驗.

難度篇:
  之前的場景比較普通, 現在讓我們加入小怪獸來攪攪局.
  在新的場景中, 小怪獸按固定線路在巡視, 鼠精靈需要走出迷宮的最少耗時是多少? "最短路徑"是多少?
  
  在有不確定的因素的干擾下, 使用常規的最短路徑算法就不再可行的. 有沒有其他的解法呢?
  在迷宮地圖較小時, 我們可以借助動態規划的思想來解決.

	設opt[n][y][x]為狀態矩陣: 
	n表示步數, (x, y)表示迷宮地圖的位置信息, 而其值表示鼠精靈在該步數后能否到達該節點.
	初始狀態:
		opt[0][y][x] = true
	狀態遷移方程:		
		opt[n+1][y][x] = (opt[n][y'][x']==true && monster[n+1] != (x', y') )==false, {ε(x',y'),adjacency to (x,y)}) ? true : false;

  具體的偽碼如下:

	// 初始化
	opt[0][y][x] = true;
	// 步數遍歷
	for ( step = 0; ; step++ ) {
		// 迷宮矩陣遍歷
		for ( i = 0; i < height; i++ ) {
			for ( j = 0; j < width; j++ ) {
				// 當前節點可達
				if ( opt[step][i][j] == true ) {
					// 枚舉各個鄰近的可達節點
					foreach (x', y') adjacency (j, i) {
						// 小怪獸在步數step + 1, 沒有走到該點
						if ( monster[step + 1] != (x', y') ) {
							opt[step + 1][y'][x'] = true;
						}
					}
				}
			}
		}
		// 檢查目標節點是否到達
		if ( opt[step][dest_y][dest_x] == true ) {
			return step;
		}
	}

	return Oops;

  注: 若該迷宮沒解, 必然存在循環節, 最外層循環借助滾動數組來優化.

總結:
  從迷宮尋路的場景出發, 逐步進行基礎知識的深挖掘, 還是具備一定的區分度的.
  面試這東西, 能遇到一個nice的面試官是種幸福. 但很多時候, 往往是一場鬧劇了.

寫在最后:
  
如果你覺得這篇文章對你有幫助, 請小小打賞下. 其實我想試試, 看看寫博客能否給自己帶來一點小小的收益. 無論多少, 都是對樓主一種由衷的肯定.

   

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM