帶你用廣度優先搜索實現地鐵計費功能

1、地鐵是如何實現收費的?

我們平時坐地鐵去上班的時候,是否有想過,地鐵是怎麼計費的呢?因爲地鐵的線路都比較複雜,一般大城市的地鐵網都是錯綜複雜的,比如廣州,目前就有15條線路,假設一條線有20個站點,那麼整個地鐵網絡就會有300個站點,因爲地鐵可以換乘,從一個起點到達一個終點,會有很多路線。我們都知道,地鐵的收費是根據距離來決定的(我們這裏先假設每兩個站的距離都是相等的),也就是站點越多,收費就越多,通常來說,地鐵從一個起點到另外一個點,都是按照最短路徑來收費的(不可能按照最長吧?),如何找到這個最短路徑呢?我們首先來看看以下簡圖:
在這裏插入圖片描述

1.1 數組實現方式

首先來看起點站和目的地是同一條地鐵線的情況,比如從A到C站,這種計算距離是十分簡單的,我們只要把整條一號線的站點組合成一個數組,就可以計算從其中某個站點到另一個站點的路程。如上圖的從A到C,假設A站的id是1,C站的id是3,我們把數組的元素設置爲id值,那麼總費用S=每站收費*(3-1),如果一站的收費是2元,那麼從A到C就是4元。這個過程十分簡單!時間複雜度是O(1),十分高效。

1.2 無向圖實現方式

起點和目的點在同一條地鐵線的情況計算是十分簡單的,只需要構造一個數組就能實現。但如果起點和目的地不是同一條線,那麼就比較複雜了。比如從一號線的A站作爲起點,去二號線的F站,可選擇的走法比較多,途中就有以下兩種走法:

路線一:A--->B--->C--->D--->E--->F

路線二:A--->B--->F

然而在現實中,兩個站點的路線可能不止兩條路線了,按照地鐵的計費原則,理應是選擇最短路徑來進行計費的,也就是上面說的路線二。那麼系統是怎麼找到路線二,並確定它就是最短路徑呢?其實,熟悉圖論的童鞋早就會想到其實地鐵線路,就是個無向圖。而無向圖中有個經典的算法是廣度優先搜索,它可以找到無向圖中兩點之間的最短路徑。

2、廣度優先搜索找到兩個站點最短路徑

爲了方便簡單實現,我們這裏用站點的ID作爲圖的各個頂點,0代表A,1代表B,2代表C,3代表D,4代表E,5代表F,這樣上面的地鐵線路圖就簡化成下面的無向圖。於是,求A—>F的最短距離問題就變成求頂點0到頂點5的最短路徑。

在這裏插入圖片描述

2.1 構建站點對象

我們先構造站點對象Station,這裏我們主要用到的是站點的stationId,如下代碼:

	/**
	 * 站點
	 */
	class Station{
		int stationId;//站點id
		String stationName;//站名
		
		public Station(int stationId,String stationName) {
			this.stationId = stationId;
			this.stationName = stationName;
		}

	}

2.2 用鄰接表構造地鐵無向圖

我們使用鄰接表來表示這個無向圖,以下是構造出的站點鄰接表。左邊的數組代表所有的站點,右邊的一個個鏈表元素代表數組中的站點鄰接的站點,比如數組中id爲2的站點,有兩個鄰接站點,分別是id爲2和4。
在這裏插入圖片描述

我們用鏈表數組stationTable表示這張鄰接表,由傳入的站點列表stationList作爲stationTable的數據源。光是站點還不夠,站點是通過一組線路來連接起來的,這個可以通過傳入兩個站點調用addEdge方法,構造站點與站點的連線。

public class SubwayGraph {
	
	int stationCount;//站點總數量
	
	LinkedList<Integer>[] stationTable;//站點鄰接表
	
	public SubwayGraph(List<Station> stationList) {
		int size = stationList.size();
		this.stationCount = size;
		this.stationTable = new LinkedList[size];
		
		for(Station s : stationList) {
			stationTable[s.stationId] = new LinkedList<>();
		}
		
	}
	
	public void addEdge(int stationId1, int stationId2) {
		stationTable[stationId1].add(stationId2);
		stationTable[stationId1].add(stationId2);
	}
	
	public int distance(int station1, int station2) {
		int distance = new StationSearch(this, station1).distanceTo(station2);
		System.out.println(String.format("站點id爲[%1$s]到站點id爲[%2$s]的距離爲[%3$s]",              station1, station2, distance));
		return distance;
	}

2.3 建立搜索類

爲了把職責分開,所以把搜索另外建一個類StationSearch,專門處理站點距離的計算。

/**
	 *站點搜索
	 */
	static class StationSearch{
		boolean[] visited;//已經經過的站點
		
		Queue<Integer> preToVisit;//準備經過的站點
		
		int sourceStationId;//起點
		
		int prevStationId[];//數組下標爲站點id,元素爲當前下標站點的上一個站點
		
		public StationSearch(SubwayGraph g, int sourceStationId) {
			int bound = g.stationCount;
			this.visited = new boolean[bound];
			this.prevStationId = new int[bound];
			this.sourceStationId = sourceStationId;
			preToVisit = new LinkedList<>();
			bfs(g,sourceStationId);//廣度優先搜索
		}
		
		/**
		 * 廣度優先搜索
		 * @param g
		 * @param sourceStationId 起點
		 */
	    private void bfs(SubwayGraph g, int sourceStationId) {
	    	visited[sourceStationId] = true;//標識爲已經過
	    	
	    	preToVisit.add(sourceStationId);
	    	
	    	while(!preToVisit.isEmpty()) {
	    		int toVisit = preToVisit.poll();
	    		
	    		for(int i : g.stationTable[toVisit]) {
	    			if(!visited[i]) {
	    				visited[i] = true;//標識爲已訪問
	    				prevStationId[i] = toVisit;//記錄上一個站點
	    				preToVisit.add(i);//鄰接站點進入隊列
	    			}
	    		}
	    	}
	    }
	    
	    /**
	     * 是否可達站點
	     * @param targetStationId
	     * @return
	     */
	    public boolean hasPathTo(int targetStationId) {
	    	return visited[targetStationId];
	    }
	    
	    /**
	     * 到指定的站點距離
	     */
	    public int distanceTo(int targetStationId) {
	    	
	    	if(!hasPathTo(targetStationId)) return 0;
	    	
	    	int distance = 0;
	    	Stack<Integer> path = new Stack<>();
	    	for(int i = targetStationId;i != this.sourceStationId;i = prevStationId[i]) {
	    		distance += 1;
	    		path.push(i);
	    	}
	    	path.push(this.sourceStationId);
	    	
	    	System.out.print("經過站點:");
	    	int size = path.size();
	    	for(int p = 0;p<size;p++) {
	    		System.out.print(path.pop() + " ");
	    	}
	    	System.out.println();
	    	return distance;
	    }
		
	}

以上代碼有幾個成員變量:

  • visited;boolean數組,默認所有元素都是false。可以想象成列車已經經過的站點,把該下標等於該經過的站點的元素記爲true。記錄訪問記錄的原因是避免重複訪問。
  • preToVisit;計劃要經過(訪問)的站點,是一個隊列。可以想象成列車可能要去往的站點。
  • sourceStationId;起點站。開始計算的站點。
  • prevStationId;是個整型數組,當前下標是當前站點,下標對應的數組元素是當前站點的上一個站點。這個變量是爲了記錄軌跡用的。

搜索類中有個最核心的方法,就是dfs方法。這個方法實現的就是廣度優先搜索。這個方法的大概邏輯分爲以下幾個步驟:

  1. 先把起點設置爲已經過(訪問),即visited[sourceStation]=true;
  2. 把起點放進待訪問隊列,即preToVisit.add(sourceStationId);
  3. preToVisit隊列中的站點出列;
  4. 將出列的站點的鄰接點遍歷出來,如果遍歷出的站點未訪問過,則把這些節點記錄爲已訪問,把它們放到隊列preToVisit中,並且把它們的上一個站點(即從隊列中poll出來的那個站點)記錄下來;
  5. 重複3、4步驟,直到隊列preToVisit爲空。即優先把當前站點的鄰接站點優先找出來,再把這些站點放進隊列,再分別把這些站點的鄰接站點優先找出來,以此類推,直到所有站點都訪問到。

再來關注一下搜索類中distanceTo方法,這裏prevStationId數組就派上用場了,因爲這個數組是記錄了當前站點訪問的上一個站點,於是可以很容易的把從起點到當前站點的軌跡打出來。這裏用了個for循環,指針變量設置爲當前的站點,不斷的自底向上的通過prevStationId[i]把所經過的站點遍歷出來,直到指針變量到達了起始點才結束。因爲整個遍歷過程是從目的地到起點的倒序,所以用棧結構來存儲這些遍歷出的站點,以便從棧中彈出來的是起點到目的地的順序。這裏還用一個distance變量來計算整個過程經歷過的“邊數”,因爲每一次遍歷可以看作是找到一根邊,一直到起點,就是所有邊的總數,也就是起點到目的站點的距離。

	   int distance = 0;
	   Stack<Integer> path = new Stack<>();
	   for(int i = targetStationId;i != this.sourceStationId;i = prevStationId[i]) {
	      distance += 1;
	      path.push(i);
	   }
	   path.push(this.sourceStationId);

2.4 測試站點距離計算

核心代碼寫完了,可以測試一下。我們先根據文章開頭那些站點建立幾個站點對象,再用這些站點構造出地鐵線路圖,把所有的站點通過調用addEdge彼此連接起來。最後一步就是計算距離,並且打印出從起點到目的地的軌跡。

	public static void main(String[] args) {
		List<Station> stationList = new ArrayList<SubwayGraph.Station>();
		//建立站點
		stationList.add(new Station(0, "A"));
		stationList.add(new Station(1, "B"));
		stationList.add(new Station(2, "C"));
		stationList.add(new Station(3, "D"));
		stationList.add(new Station(4, "E"));
		stationList.add(new Station(5, "F"));
		
		//構造地鐵線路圖
		SubwayGraph g = new SubwayGraph(stationList);
		g.addEdge(0, 1);//A-B
		g.addEdge(1, 2);//B-C
		g.addEdge(1, 4);//B-E
		g.addEdge(2, 3);//C-D
		g.addEdge(3, 4);//D-E
		g.addEdge(4, 5);//E-F
		
		g.distance(0, 5);//計算站點0到5的距離
	}

以下爲打印結果:

經過站點:0 1 4 5 
站點id爲[0]到站點id爲[5]的距離爲:3

2.5 爲什麼廣度優先搜索的路徑是最短路徑

爲什麼廣度優先搜索的路徑是最短路徑呢?換句話說,爲什麼沒有其他線路比當前計算出來的路線短?因爲從廣度優先搜索算法的實現來看,我們可以發現一個特點:隊列preToVisit中的元素,都是按照它們和起點的距離順序進隊列的,距離起點站越近的總會最先出列。所以在目的站點targetStationId進隊列到它出隊列期間,不可能有別的更短路徑到達targetStation。

3、廣度優先搜索的性能

3.1 時間複雜度

這種廣度優先搜索的時間複雜度是多少呢?我們假設最壞的情況,就是無向圖中最後一步才找到目的站點,這時候意味着圖中每個頂點,以及每個頂點的鄰接點都訪問了一次,也就是相當於每條邊都訪問了兩次,如果無向圖的邊數是E,那麼廣度優先搜索最壞的情況所需要的時間是2E。而平均來看,廣度優先的搜索時間複雜度是線性的O(E)。

3.2 空間複雜度

實現一個無向圖,用了鄰接表的存儲結構。假設總的頂點數是N,鄰接表除了要存儲各個頂點,而且還要存儲頂點的鄰接點,另外還有額外的visitedpreToVisitprevStationId這幾個結構,但空間規模都不會超過N,所以廣度優先算法的空間複雜度是O(N)。

4、實現更好的地鐵計費系統

其實上面的實現,不是最優的計費系統實現方式。正如我們剛開始假設的,如果起點和目的地是同一條路線,那麼我們可以通過數組訪問實現時間複雜度爲常數的計算,而不用經歷廣度優先搜索。

在這裏插入圖片描述

5、本文源碼地址

戳這裏獲取本文源代碼:點我

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章