有天晚上还没睡着的时候,突然想起以前做课程设计时,有同学搞那个公交线路查询,老师上课时还提过什么只能查出换乘两次的线路,我不知道是那程序限制了换乘的次数还是那个算法查不出换乘两次以上的线路了,如果是后者,那个算法就有点糟糕。后来就想,如果给我做的话怎么做呢,别人写公交查询,我这个列车迷就写个地铁线路查询,其实感觉地铁的比公交的简单多了。
这样的线路查询,说白了其实也是图的遍历问题,大二学数据结构的时候,在课上老师有说到图的遍历算法能解决线路查询问题,也说到某些物体移动的动画,图能搞出来。后者我完全不明白了。前者我现在还能用得上。
最直接的方法,就把各个站点作为一个结点连在一起,成为一个图,像这样(估计有不少园友对这幅图很熟悉)
相邻的两个站点互为可达,线路查找时就通过图的深度遍历或广度遍历查找出出发点和目的地的线路。这种算法直接明了,简单易写,可是效率不高。我也写了一个,用来作为参考。
我调整后的算法思路是这样的,先不理这个站点的下一个或上一个站点是什么,我只管这个站点在哪条线路上,把起点和终点的线路找出来,用线路作为图的结点,能换乘的两条线路互为可达,像这样(这幅图比上一幅丑多了,莫笑)
同样也是通过图的深度或广度遍历查找出线路,由于地铁线路肯定比地铁站要少很多,对线路图的遍历会比站点图的遍历要快。而本算法采用的是图的广度遍历算法,其实应该最好用层次遍历的,但是没用上,估计速度会更快。
好了,放了文字又放图,怎么能少得了代码,既然是算法,肯定要有数据结构,先把实体类列出来
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("沙园", "祖庙"); }
上面的时间是线路图遍历的,下面那个是站点图遍历的,而且这个结果很稳定,都是线路图完胜的。
当初写这个算法时与某个饭说过,我要写佛*市的地铁查询线路,她不屑一顾,也许是水平太低了吧,其实我是写的是通用地铁的线路查询,没局限在一个城市,只要地铁线网的数据正确就行了。第一次写算法的博文,太浅显的内容了,主要是对算法的研究不深入,写过的算法不多。这些本是在校时同学们写的东西,在这个时候我却拿来自娱自乐。当初学数据结构的时候没学好,觉得辜负教我们数据结构的张老师,在大三大四几次重要场合碰过面,最后一次是答辩时,我表现极差,大糗了一场。能有什么改进的,还请各位园友指出。谢谢!