地铁线路最短路径(项目实现)
文章目录
项目需求
- 实现一个帮助用户出行时进行地铁路线规划推荐的程序
- 支持向用户推荐任意两站之间通过最少站数的路线
- 支持查询单条线路的所有站点
实现语言
系统整体的算法核心主要使用Java语言来实现,可通过命令行进行数据交互。通过指定的地图数据和相关查询指令,可以实现所有的需求。
实现算法
通过对该项目的分析,我们不难发现这是最经典的求最短路径问题。所谓最短路径问题是指:如果从图中某一顶点到达另一顶点的路径可能不止一条,如何找到一条路径使得沿此路径上各边的权值总和达到最小。在本问题中,我们是要完成从地铁图中找到一条路径使得从某一起始站到达另一终点站的路径长度最小,也就是经过的站点最少。我们这里假设相邻两站之间的权重相同。那么我们解决这类问题最常用的算法有DFS/BFS算法、Floyd算法、Dijksta算法。针对该问题,我对这几类算法进行了分析:
DFS/BFS算法
:适合解决解决单源最短路径,从起始结点开始访问所有的深度遍历路径或广度优先路径,则到达终点结点的路径有多条,取其中路径权值最短的一条则为最短路径。DFS比较适合判断图中是否有环,寻找两个节点之间的路径,有向无环图(DAG)的拓扑排序,寻找所有强连通片(SCC),无向图中寻找割点和桥等;而BFS则比较适合判断二分图,以及用于实现寻找最小生成树(MST),如在BFS基础上的Kruskal算法。还有寻找最短路径问题(如Dijkstra算法)。Floyd算法
:Floyd算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法。而我们这里每站之间的权值是一样的,显然我们可以选择用更简单的算法,所以不考虑使用该算法。Dijksta算法
:Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。
通过上述分析,再加之地铁地图属于稀疏图,而且该图中每条边的权值恒定,所以我决定用最为简单也较为适合该项目的BFS
算法来实现。
存储设计
在数据存储上,主要是存点或者存边两种形式。由于不同的铁路路线存在着相同的站点,即为换乘站点,如果采用存点的形式,则数据会有冗余,而且还要提前判断是那条铁路路线;如果采用存边的形式,由于边是唯一的,而换乘站点可以用两条铁路线的站点名相同来判断,同一条边不会存在两条不同的路线上,是数据不会有冗余,并且数据也不需要有序。因此我在该项目的实现中,选择的存边的形式。每个模块由一个#
开始,紧跟着的是这条路线的名称。接下来包含若干条该路线的边的信息。
样例如下:
#1号线 苹果园 古城 八角游乐园 玉泉路 ...
#2号线 西直门 积水潭 鼓楼大街 安定门 ...
...(省略)
文件存放
地铁线路信息保存在data.txt
中,格式如下:
#线路名1 站名1 站名2 站名3 ...
#线路名2 站名1 站名2 站名3 ...
#线路名3 站名1 站名2 站名3 ...
查询单条线路的所有站点(从起始站点开始到终点站)的结果保存在station.txt
文件中,格式如下:
1号线:
苹果园-->古城-->八角游乐园-->...-->四惠东
给出起点和终点,保存最短路径到routine.txt
文件中,格式如下:
从天安门东到圆明园共经过14站
(1号线)
天安门东
天安门西
西单
复兴门
(换乘2号线)
阜成门
车公庄
西直门
(换乘13号线)
大钟寺
知春路
(换乘10号线)
知春里
海淀黄庄
(换乘4号线大兴线)
中关村
北京大学东门
圆明园
职责划分
-
public class Station
:这个类中主要是定义站点的相关信息,和实现get和set方法。 -
public class Subway
:这个类主要是代码的实现。private static void printf(Object data) throws IOException
:实现把内容打印到文本文件中。private static int getStationId(String stationName)
:获取站点ID,每个站点ID唯一。private static int getEdgeHash(int a, int b)
:用该函数来唯一标识每一个条边。private static void addEdge(int id1,int id2,String lineName)
:添加站点与站点之间的边。private static void data_init(String map)
:数据初始化。private static void getLine(String lineName) throws IOException
:获取指定地铁路线的所有站点。private static String checkSwitchLine(int id1, int id2, int id3)
:利用Hash的唯一性,可以判断这条边唯一属于的路线,如果判断路线一样则不用换乘,否则需要换乘。private static void getStationPath(String startStation,String endStation) throws IOException
:获取两个站点的最短路径。
核心代码
由于程序已上传至GitHub上,这里主要就程序中一些核心的函数进行详细讲解。
-
定义结构体
这里定义了站点的结构体,里面包含了站点的唯一ID标识、站点名称、站点属于的路线、相邻的站点和实现BFS算法时需要使用到的visited和lastPoint。
Station(int id, String name){
this.id = id; //站点ID
this.name = name; //站点名
this.lines = new HashSet<>(); //记录站点属于的路线,可能会同时属于好几条路线,即为换成车站
this.edges = new ArrayList<>(); //记录相邻站点
this.visited = false; //判断是否已经访问过
this.lastPoint = 0; //再BFS时记录上一站点的ID
}
-
数据初始化
读取地铁的数据文件并初始化数据。该函数主要是提取地铁数据文件中有用的信息,通过判断每行的首个元素是否含有#
来判断是否是地铁路线名,然后用lineStart
来记录这条路线上的起始站,方便查询整条线路的时候使用。
/**
* 数据初始化
* @param map
*/
private static void data_init(String map){
try(InputStreamReader isr = new InputStreamReader(new FileInputStream(map), "UTF-8");BufferedReader br=new BufferedReader(isr)) {
String line;
String lineName="";
while ((line = br.readLine())!=null){
String[] list = line.split(" ");
if (list[0].contains("#")){
lineName = list[0].substring(1);
lineStart.put(lineName,getStationId(list[1]));
stations.get(getStationId(list[1])).addLines(lineName);
}
for (int i=1; i < list.length-1; i++){
int id1=getStationId(list[i]);
int id2=getStationId(list[i+1]);
stations.get(id2).addLines(lineName);
addEdge(id1, id2, lineName);
}
}
Station.count=stationCount;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
-
打印函数
打印结果,判断用户是否有使用-o
指令,如果有则将结果同步打印到目标文件中。
/**
* 打印到文本文件中
* @param data
* @throws IOException
*/
private static void printf(Object data) throws IOException{
String text = String.valueOf(data);
if (o!=null){
BufferedWriter out = new BufferedWriter(new FileWriter(o,true)); //写在文件的末尾处
out.write(text);
System.out.print(text);
out.close();
}else {
System.out.print(text);
}
}
-
输出指定路线上的所有站点
传入路线名参数lineName
,先判断是否存在该路线,如果存在则利用队列来存储这条路线上的所有站点ID,并输出。
/**
* 获取指定地铁路线的所有站点
* @param lineName
* @throws IOException
*/
private static void getLine(String lineName) throws IOException {
if (!lineStart.containsKey(lineName)){
printf("error:不存在这条路线。");
return;
}
printf(lineName+":"+"\n");
int start=lineStart.get(lineName);
Queue<Integer> q = new LinkedList<>();
q.offer(start);
stations.get(start).setVisited(true);
boolean first=true;
while (!q.isEmpty()){
int now=q.poll();
Station nowStation= stations.get(now);
if (first){
printf(nowStation.getName());
first=false;
}else {
printf("-->"+nowStation.getName());
}
for (int it:nowStation.getEdges()){
Station tmp=stations.get(it);
if (tmp.isVisited()) continue;
if (tmp.checkLine(lineName)){
q.offer(it);
tmp.setVisited(true);
}
}
}
}
-
输出两站点间的最短路径
利用BFS
算法来实现地铁路线最短路径的搜索,并通过checkSwitchLine(int id1, int id2, int id3)
函数来判断是否需要换乘,最后将结果输出。
/**
* 获取两个站点的最短路径
* @param startStation 起始站
* @param endStation 终点站
* @throws IOException
*/
private static void getStationPath(String startStation,String endStation) throws IOException {
int startID=getStationId(startStation);
if (startID>Station.count){
printf("error:没有"+startStation+"这个站点。");
return;
}
int endID=getStationId(endStation);
if (endID>Station.count){
printf("error:没有"+endStation+"这个站点。");
return;
}
Queue<Integer> q=new LinkedList<>();
q.offer(startID);
stations.get(startID).setVisited(true);
while (!q.isEmpty()){
Station now=stations.get(q.poll());
for (int it:now.getEdges()){
Station tmp=stations.get(it);
if (tmp.isVisited()) continue;
q.offer(it);
tmp.setVisited(true);
tmp.setLastPoint(now.getId());
if (it==endID) break;
}
}
//回溯找到最短路径
Stack<Integer> path=new Stack<>();
int step=endID;
while (step!=startID){
path.push(step);
step=stations.get(step).getLastPoint();
}
//记录经过的各个站点的唯一ID
ArrayList<Integer> res=new ArrayList<>();
res.add(startID);
while(!path.empty()){
int nowStationID=path.pop();
res.add(nowStationID);
}
printf("从"+startStation+"到"+endStation+"共经过"+res.size()+"站\n"); //打印一共经过了几个站点
printf("("+edgeLines.get(getEdgeHash(res.get(0), res.get(1)))+")\n");
//判断是否是换乘车站
int id1=-1,id2=-1;
for(int it:res){
if(id1!=-1){
String switchMsg=checkSwitchLine(id1,id2,it);
if(switchMsg!=null){
printf("(换乘"+switchMsg+")\n");
}
}
printf(stations.get(it).getName()+"\n");
id1=id2;
id2=it;
}
}
checkSwitchLine函数实现:
/**
* 利用Hash的唯一性,可以判断这条边唯一属于的路线,如果判断路线一样则不用换乘,否则需要换乘
* @param id1
* @param id2
* @param id3
* @return
*/
private static String checkSwitchLine(int id1, int id2, int id3){
String linea=edgeLines.get(getEdgeHash(id1,id2));
String lineb=edgeLines.get(getEdgeHash(id2,id3));
if(linea.equals(lineb)) return null;
return lineb;
}
getEdgeHash函数实现:
/**
* 利用hash的特点来唯一标识每一个条边
* @param a
* @param b
* @return
*/
private static int getEdgeHash(int a, int b){
return a*HASH_BASE+b;
}
运行效果
通过-a
命令显示一条线路上所有的站点。
通过-b
命令查询两站之间的最短路径。
通过-o
命令指定结果输出的文件。
对不合法命令的检测。
总结
- 这次大作业让我知道了自己对Java的不熟练,需要继续学习掌握并熟练运用Java语言。
- 让我重新回顾了数据结构中所学的内容。
- 在规定的时间内完成一个小项目,算是对自身的考验吧,希望下一次作业能完成的更好。
- 第一次以博客的形式完成作业,也学会了git一些小知识。