有天晚上還沒睡着的時候,突然想起以前做課程設計時,有同學搞那個公交線路查詢,老師上課時還提過什么只能查出換乘兩次的線路,我不知道是那程序限制了換乘的次數還是那個算法查不出換乘兩次以上的線路了,如果是后者,那個算法就有點糟糕。后來就想,如果給我做的話怎么做呢,別人寫公交查詢,我這個列車迷就寫個地鐵線路查詢,其實感覺地鐵的比公交的簡單多了。
這樣的線路查詢,說白了其實也是圖的遍歷問題,大二學數據結構的時候,在課上老師有說到圖的遍歷算法能解決線路查詢問題,也說到某些物體移動的動畫,圖能搞出來。后者我完全不明白了。前者我現在還能用得上。
最直接的方法,就把各個站點作為一個結點連在一起,成為一個圖,像這樣(估計有不少園友對這幅圖很熟悉)
相鄰的兩個站點互為可達,線路查找時就通過圖的深度遍歷或廣度遍歷查找出出發點和目的地的線路。這種算法直接明了,簡單易寫,可是效率不高。我也寫了一個,用來作為參考。
我調整后的算法思路是這樣的,先不理這個站點的下一個或上一個站點是什么,我只管這個站點在哪條線路上,把起點和終點的線路找出來,用線路作為圖的結點,能換乘的兩條線路互為可達,像這樣(這幅圖比上一幅丑多了,莫笑)
同樣也是通過圖的深度或廣度遍歷查找出線路,由於地鐵線路肯定比地鐵站要少很多,對線路圖的遍歷會比站點圖的遍歷要快。而本算法采用的是圖的廣度遍歷算法,其實應該最好用層次遍歷的,但是沒用上,估計速度會更快。
好了,放了文字又放圖,怎么能少得了代碼,既然是算法,肯定要有數據結構,先把實體類列出來
1 public class StationEnt 2 { 3 public string StationName { get; set; } 4 public Dictionary<LineEnt,int> PlaceLine { get; set; } 5 }
首先是站點的,里面有兩個成員,一個是站點名,另一個是所在線路,一個站當然可以位於多條線路里面。因為這里沒有上一站,下一站這樣的結構,所以在線路后面加了一個編號來確定站點所在的位置,同時一個站點沒理由重復位於同一條線路的,所以這里用了個dictionary的泛型。
1 public class LineEnt 2 { 3 public string LineName { get; set; } 4 public List<StationEnt> Stations { get; set; } 5 public List<Tuple<LineEnt,StationEnt>> TranformStations { get; set; } 6 public bool IsRoundLine { get; set; } 7 }
有站點當然要有線路,線路的成員有四個,一個是線路名,線路包括站點的集合,線路的換乘站,還有最后一個屬性是標識這條線路是否環線,因為環線會有另一種的處理方式。換乘站集合用了一個二元組的List存儲,考慮到兩條線路的換乘站有可能不止一個,而換乘站又要知道是換乘哪條線路的。
1 public class Station2Station 2 { 3 public StationEnt FromStation { get; set; } 4 public LineEnt Line { get; set; } 5 public StationEnt ToStation { get; set; } 6 }
還有一個,用於線路查詢時的,方便記錄路線,這只是整條線路中的一段,看字面意思都會明白,起點站和到達站,還有通過的線路。
整個算法封裝在一個類中:MetroNetModel。為了減少堆內存的使用量,這里都用了引用類型,對於某個站點、某條線路這些實例,整個類里面只有一個。
下面是類的私有字段
1 /// <summary> 2 /// 全線網站點集合 3 /// </summary> 4 protected Dictionary<string, StationEnt> _stationCollection; 5 /// <summary> 6 /// 全線網線路集合 7 /// </summary> 8 protected List<LineEnt> _lineCollection;
這兩個是對於一個地鐵線路網是必有的
1 /// <summary> 2 /// 最短線路的站點數 3 /// </summary> 4 protected int _minLine; 5 6 /// <summary> 7 /// 最短換乘次數 8 /// </summary> 9 protected int _minTransCount; 10 11 /// <summary> 12 /// 最短的線路段集合 13 /// </summary> 14 protected List<List<Station2Station>> _shortestLines; 15 /// <summary> 16 /// 最短的線路集合 17 /// </summary> 18 protected List<List<StationEnt>> _shortestWays;
這幾個是查詢中要用到的字段
類的構造函數如下,初始化各個集合,最后調用的FieltLines()方法是給站點集合和線路集合填充對象的,算是讀取數據的方法吧!
1 public MetroNetModel2() 2 { 3 _minLine = _minTransCount=int.MaxValue; 4 _shortestLines = new List<List<Station2Station>>(); 5 _shortestWays = new List<List<StationEnt>>(); 6 _lineCollection = new List<LineEnt>(); 7 _stationCollection = new Dictionary<string, StationEnt>(); 8 FieltLines(); 9 }
整個類就只有一個公共的方法GuedeMetroWay2(string fromStation, string toStation),輸入的是起點站和目標站的名稱。方法體如下
1 public string GuedeMetroWay2(string fromStation, string toStation) 2 { 3 //驗證站點存在 4 if (!_stationCollection.ContainsKey(fromStation)) 5 return fromStation + " is not contain"; 6 if (!_stationCollection.ContainsKey(toStation)) 7 return toStation + " is not contain"; 8 if (fromStation == toStation) return fromStation; 9 10 StationEnt start = _stationCollection[fromStation]; 11 StationEnt end = _stationCollection[toStation]; 12 List<Station2Station> stationList; 13 List<LineEnt> lineHis; 14 15 //重調兩個最值 16 _minLine = _minTransCount = int.MaxValue; 17 18 //遍歷這個起點站所在的線路,然后分別從這些線路出發去尋找目的站點 19 foreach (KeyValuePair<LineEnt,int> line in start.PlaceLine) 20 { 21 stationList = new List<Station2Station>(); 22 lineHis = new List<LineEnt>() { line.Key }; 23 GuideWay2(0, start, line.Key, end, stationList, lineHis); 24 } 25 //去除站點較多的線路 26 ClearLongerWays(); 27 //生成線路的字符串 28 string result = ConvertStationList2String(); 29 30 //清空整個查找過程中線路數據 31 _shortestLines.Clear(); 32 _shortestWays.Clear(); 33 34 return result; 35 }
由於不知道各個站間的時間間隔,算法中只能按站點的數量來判定哪條線路更快,這樣可能就是與百度上查找的結果有出入的原因吧!
上面查找的核心方法是GuideWay2,它是一個圖的廣度遍歷的遞歸算法,傳入的參數分別是當前換乘次數,當前站,當前線路,目標站,途徑線路段的集合,已經到過的線路,方法定義如下
1 protected void GuideWay2(int transLv, StationEnt curStation, LineEnt curLine, 2 StationEnt endStation, List<Station2Station> stationList, 3 List<LineEnt> lineHis) 4 { 5 //如果當前換乘的次數比換乘次數最小值 6 //就不用再找了,找出來的線路肯定更長 7 if (transLv > _minTransCount) return; 8 //判定是否已經到達目標站的線路了,若是表明一直查找成功了 9 if (IsSameLine2(curStation, endStation,curLine)) 10 { 11 Station2Station s2s = new Station2Station() 12 { FromStation = curStation, Line = curLine, ToStation = endStation }; 13 stationList.Add(s2s); 14 //若當前換乘次數比記錄值要小,清空之前的線路段 15 if (_minTransCount > transLv) 16 _shortestLines.Clear(); 17 18 _shortestLines.Add(stationList.ToArray().ToList()); 19 stationList.Remove(s2s); 20 _minTransCount = transLv; 21 return; 22 } 23 List<Tuple<LineEnt, StationEnt>> transform = curLine.TranformStations; 24 //遍歷一下當前線路的換乘站,從而遞歸找出到目標站的線路 25 foreach (Tuple<LineEnt, StationEnt> item in transform) 26 { 27 //如果這條線路已經到過的,進入下次循環 28 if (lineHis.Contains(item.Item1)) continue; 29 30 31 32 lineHis.Add(item.Item1); 33 Station2Station s2s = new Station2Station() 34 { FromStation = curStation, Line = curLine, ToStation = item.Item2 }; 35 stationList.Add(s2s); 36 //遞歸調用 37 GuideWay2(transLv + 1, item.Item2, item.Item1, endStation, stationList, lineHis); 38 //清除集合里的值,以這種方式減少內存使用量,提高效率 39 lineHis.Remove(item.Item1); 40 stationList.Remove(s2s); 41 } 42 }
下面是其他輔助的方法,不作一一介紹了
1 /// <summary> 2 /// 清除站點較多的線路 3 /// </summary> 4 protected void ClearLongerWays() 5 { 6 _shortestWays.Clear(); 7 int curCount = 0; 8 List<StationEnt> way = null; 9 List<StationEnt> temp=null; 10 foreach (List<Station2Station> innerList in _shortestLines) 11 { 12 curCount = 0; 13 way = new List<StationEnt>(); 14 foreach (Station2Station item in innerList) 15 { 16 temp = GetWayStations(item.FromStation, item.ToStation, item.Line); 17 curCount += temp.Count; 18 if (curCount > _minLine) break; 19 way.AddRange(temp); 20 } 21 if (curCount == _minLine) 22 _shortestWays.Add(way); 23 else if (curCount < _minLine) 24 { 25 _shortestWays.Clear(); 26 _shortestWays.Add(way); 27 _minLine = curCount; 28 } 29 } 30 } 31 32 /// <summary> 33 /// 把線路段轉換成字符串 34 /// </summary> 35 /// <returns></returns> 36 protected string ConvertStationList2String() 37 { 38 string result = string.Empty; 39 foreach (List<StationEnt> innerList in _shortestWays) 40 { 41 foreach (StationEnt item in innerList) 42 { 43 result += item.StationName + " ==> "; 44 } 45 result += " \r\n\r\n "; 46 } 47 result = result.Trim('\n').Trim('\r').Trim('\n').Trim('\r'); 48 return result; 49 } 50 51 /// <summary> 52 /// 判定兩個站是否存在給定線路中 53 /// </summary> 54 /// <param name="station1"></param> 55 /// <param name="station2"></param> 56 /// <param name="line"></param> 57 /// <returns></returns> 58 protected bool IsSameLine2(StationEnt station1, StationEnt station2, LineEnt line) 59 { 60 bool result = line.Stations.Contains(station1) && line.Stations.Contains(station2); 61 return result; 62 } 63 64 /// <summary> 65 /// 獲取站點1和站點2在給定線路上最短的途徑站點集合 66 /// </summary> 67 /// <param name="station1"></param> 68 /// <param name="station2"></param> 69 /// <param name="line"></param> 70 /// <param name="flag"></param> 71 /// <returns></returns> 72 protected List<StationEnt> GetWayStations(StationEnt station1, StationEnt station2, LineEnt line, bool flag = true) 73 { 74 List<StationEnt> result = new List<StationEnt>(); 75 int sIndex, eIndex; 76 //對於環線作的處理 77 if (line.IsRoundLine && flag) 78 { 79 int stationCount = line.Stations.Count + 1; 80 int forwardCount = station1.PlaceLine[line] - station2.PlaceLine[line]; 81 int opposite = stationCount - forwardCount; 82 83 if (Math.Abs(forwardCount) > Math.Abs(opposite)) 84 { 85 result.AddRange(GetWayStations(station1, line.Stations.First(), line, false)); 86 result.AddRange(GetWayStations(line.Stations.First(), station2, line, false)); 87 return result; 88 } 89 } 90 sIndex = station1.PlaceLine[line]; 91 eIndex = station2.PlaceLine[line]; 92 List<StationEnt> stations = line.Stations; 93 if (station1.PlaceLine[line] <= station2.PlaceLine[line]) 94 { 95 for (int i = sIndex; i <= eIndex; i++) 96 result.Add(stations[i]); 97 } 98 else 99 { 100 for (int i = sIndex; i >= eIndex; i--) 101 result.Add(stations[i]); 102 } 103 return result; 104 }
Main方法里的測試代碼
1 Model.MetroNetModel2 metro = new Model.MetroNetModel2(); 2 DateTime s = DateTime.Now; 3 4 Console.WriteLine(metro.GuedeMetroWay2("祖廟", "鷺江")); 5 Console.WriteLine(metro.GuedeMetroWay2("祖廟", "三元里")); 6 Console.WriteLine(metro.GuedeMetroWay2("祖廟", "沙園")); 7 Console.WriteLine(metro.GuedeMetroWay2("祖廟", "五山")); 8 Console.WriteLine(metro.GuedeMetroWay2("鷺江", "祖廟")); 9 Console.WriteLine(metro.GuedeMetroWay2("大學城北", "祖廟")); 10 Console.WriteLine(metro.GuedeMetroWay2("祖廟", "嘉禾望崗")); 11 Console.WriteLine(metro.GuedeMetroWay2("烈士陵園", "中大")); 12 Console.WriteLine(metro.GuedeMetroWay2("林和西", "體育中心")); 13 Console.WriteLine(metro.GuedeMetroWay2("林和西", "海心沙")); 14 Console.WriteLine(metro.GuedeMetroWay2("體育中心", "海心沙")); 15 Console.WriteLine(metro.GuedeMetroWay2("體育中心", "天河南")); 16 DateTime e = DateTime.Now; 17 Console.WriteLine(e - s); 18 Console.ReadLine();
運行結果圖
由於不懂得如何衡量一個算法的優劣,只會通過起止時間的間隔來判斷,此外我還寫了個最原始的遍歷站點的算法來作參照,執行同樣的線路查詢,心里有些忐忑,上面第一幅圖是我改的算法,第二幅是遍歷站點的算法。因為發現差別不明顯,時間上是差不多的,偶爾站點圖的遍歷還會比線路圖的遍歷要快(都是這堆查詢)。直到讓某個查詢重復執行10000次,看了結果,我才松了口氣
for (int i = 0; i < 10000; i++) { metro.GuedeMetroWay2("沙園", "祖廟"); }
上面的時間是線路圖遍歷的,下面那個是站點圖遍歷的,而且這個結果很穩定,都是線路圖完勝的。
當初寫這個算法時與某個飯說過,我要寫佛*市的地鐵查詢線路,她不屑一顧,也許是水平太低了吧,其實我是寫的是通用地鐵的線路查詢,沒局限在一個城市,只要地鐵線網的數據正確就行了。第一次寫算法的博文,太淺顯的內容了,主要是對算法的研究不深入,寫過的算法不多。這些本是在校時同學們寫的東西,在這個時候我卻拿來自娛自樂。當初學數據結構的時候沒學好,覺得辜負教我們數據結構的張老師,在大三大四幾次重要場合碰過面,最后一次是答辯時,我表現極差,大糗了一場。能有什么改進的,還請各位園友指出。謝謝!