Subway
Subway
2018 年秋季学期《软件工程基础》课程双人项目作业
在北京地铁线路图或其他给定线路图上完成一系列最短路等问题求解,运行于 Windows 平台,使用 C# 基于 .NET Framework 4.5 开发。
GitHub 项目地址:https://github.com/lytning98/subway
文章目录
另一位同学的 Blog 地址:
一、PSP 表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 20 |
- Esitimate | 估计任务时间 | 30 | 20 |
Development | 开发 | 1,095 | 1,295 |
- Analysis | 需求分析 | 45 | 30 |
- Design Spec | 生成设计文档 | 60 | 100 |
- Design Review | 设计复审 | 20 | 10 |
- Coding Standard | 代码规范 | 30 | 45 |
- Design | 具体设计 | 180 | 210 |
- Coding | 具体编码 | 600 | 800 |
- Code Review | 代码复审 | 60 | 40 |
- Test | 测试(自我测试、修改代码、提交修改) | 100 | 60 |
Reporting | 报告 | 260 | 425 |
- Test Report | 测试报告 | 50 | 40 |
- Size Measurement | 计算工作量 | 10 | 30 |
- Postmortem & Process Improvement Plan | 事后总结并提出过程改进计划 | 190 | 355 |
合计 | 1,385 | 1,740 |
二、小组分工
第一人(学号 1120162453,即本文作者):
- 最短路和遍历等图论算法编写、路径输出实现;
- 地铁地图信息的整合;
- 地铁地图信息与图论模型之间的转换;
第二人(学号112016):
- GUI 设计和 GUI 逻辑的实现;
- 地铁地图 JSON 信息的读取、保存;
- 实现对地铁地图信息的查询,整合各个模块功能;
三、图论算法模块实现简介
本部分代码位于命名空间 subway.Graph
,相关实现代码位于 /subway/Graph/
目录下。
地铁站点之间的线路网构成了一张图论中的无向连通图,因此项目涉及到的求最短路径、求最短遍历路径两个需求使用一些图论算法来解决十分合适。下文将分别进行说明。
1. 图论基础数据结构
本项目中无向图使用邻接表(Adjacent List)方式存储图中的节点和边,由于需要求解最短路,边中还需要保存边权。
存储边信息的类是 subway.Graph.Edge
,其实现位于 /subway/Graph/Edge.cs
中,部分代码如下。
class Edge
{
public int From { get; set; }
public int To { get; set; }
public int Dis { get; set; }
public Edge(int from, int to, int dis)
{
From = from;
To = to;
Dis = dis;
}
}
为了方便保存路径,本项目还编写了一个用于保存路径的类 subway.Graph.Path
,该类继承自 C# 泛型容器 List,保存一系列节点编号形成一条路径,并提供了 toString()
方法便于转换成字符串进行输出。相关实现位于/subway/Graph/Path.cs
中,部分代码如下。
class Path : List<Tuple<int, int>>
{
/// <summary>
/// 根据地铁站名生成行程描述字符串
/// </summary>
/// <returns>描述字符串</returns>
public string ToPathString(bool transition = true)
{
string res = this.Count.ToString();
int lastStationId = -1;
foreach (Tuple<int, int> t in this)
{
if (t.Item1 == lastStationId)
{
if(transition)
res += " 换乘" + Map.subwayMap.Lines[t.Item2].Name;
}
else
{
res += "\n" + Map.subwayMap.Stations[t.Item1].Name;
}
lastStationId = t.Item1;
}
return res;
}
}
其余的图论模型相关处理都位于文件 subway/Graph/GraphSolver.cs
中,包括建图的相关处理等。
2. 最短路问题
作业文档中给出的需求是:求给定两站之间的最短路径,距离以经过的站数来衡量,换乘其他地铁线路算作经过 3 站。
最短路问题是图论中的经典问题,有许多高效算法,本程序中使用的是 SPFA (Shortest Path Faster Algorithm)算法,其在无向图中有近似线性的时间复杂度,同时便于记录最短路径。
但本项目中换乘带来的额外距离并不是非常容易处理,记录乘客当前所在线路会极大的降低算法的效率,并使得实现更为繁琐。程序最终采用了图论模型中常用的一种“拆点”策略,对于多线交汇的换乘站,使用无向图中的多个点来表征实际存在的一个地铁站点,并在这些点之间建立距离为 3 个单位的无向边,代表换乘。
如上图例子所示,两线交汇的”郭公庄“地铁站被对应到了无向图中的两个点,分别与两条线路上的邻接站点建立距离为 1 的无向边,同时两个点之间也建立距离为 3 的无向边。在这张无向图中,”大葆台“到”丰台科技园“的最短路径长度为 5 ,正确计入的换乘消耗。
按如上方式正确建图后,从指定的起点出发运行 SPFA 算法即可得到最短路径。SPFA 算法的细节此处不再详述。
最短路算法的实现位于文件 subway/Graph/GraphSolver.SPFA.cs
中,其实现借助了 C# 泛型 List、Queue 等容器,SPFA 核心算法的实现函数如下:
private static void SPFA(int source)
{
foreach (var kv in graphNodeDic[source])
{
dist[kv.Value] = 0;
Q.Enqueue(kv.Value);
inQueue[kv.Value] = true;
}
while (Q.Count != 0)
{
int x = Q.Dequeue();
inQueue[x] = false;
foreach (Edge e in adjList[x])
{
if (dist[e.To] > dist[e.From] + e.Dis)
{
dist[e.To] = dist[e.From] + e.Dis;
prev[e.To] = e.From;
if (!inQueue[e.To])
{
inQueue[e.To] = true;
Q.Enqueue(e.To);
}
}
}
}
3. 遍历问题
作业文档中给出的要求是:求从指定站点出发,经过所有站点至少一次的最短路径。
本问题与经典的 哈密顿通路问题 相似,尽管没有”每个点必须经过恰好一次“的限制,但实际上与原问题差别不大,仍然属于 NP 完全问题 之一,目前不存在多项式时间复杂度的求解算法,而指数级时间复杂度的算法显然不能承受北京地铁近 400 站的规模,因此只能通过一些最优化方法(如贪心算法或蚁群算法等)求得近似解。
本项目最终采用的是一种贪心策略,每次选择始发站离当前位置最近的一条线路,前往该条线路的始发站并走完全程,直到遍历完所有线路为止。
该过程的实现位于 /subway/Graph/GraphSolver.cs
文件中的 TravelAroundPath
函数中。
四、地铁信息文件组织
本项目中地铁的地图信息使用 JSON(JavaScript Object Notation)保存在文本文件*读取和使用。
由于作业对 GUI 方面的要求需要近四百个地铁站里每一个地铁站的位置坐标数据,而相关信息(尤其是其在地图中的坐标信息)很难使用自动化手段获取并整理,且这一部分数据获取的枯燥机械化工作也不属于软件工程需要实践训练之处,因此本项目的北京地铁地图 JSON 数据直接使用了开源网站 GitHub 上其他人的 JSON 文件(注:链接 )。
1. JSON 简介
JSON 类似于 XML,是一种轻量级的数据交换格式,结构清晰简单,易于人阅读和编写,同时也易于机器解析和生成。本项目中使用的 JSON 文件部分截取如下。
{
"title": "北京地铁线路图",
"stations": [
{
"x": 166,
"y": 339,
"name": "苹果园"
},
{
"x": 185,
"y": 360,
"name": "古城"
}
]
}
JSON 中包含的基本信息包括所有站点的名称、坐标和所有地铁线路的沿途站点。每个地铁站上通过的线路和地铁站之间的连接关系在程序中由这些基本信息计算而来。
2. JSON 解析
本项目引入了 Newtonsoft 开发的第三方运行库解析 JSON 字符串,因此可执行文件 subway.exe
所在目录下必须存在该运行库的动态链接库文件 Newtonsoft.JSON.dll
。
JSON 是一种对象描述语言,因此需要事先按照 JSON 的格式定义好所需的类,供解析程序将具体数据解析至实例化后的 JSON 对象中,形象化的说,类似于通过定义数据类搭建骨架、并调用运行库将数据填入这个骨架。
本项目中用于承载 JSON 对象数据的类定义是位于 /subway/JsonClass
文件夹中,现取其中的 Subway.cs
中的类定义代码演示如下:
class Subway
{
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("stations")]
public List<SubwayStation> Stations { get; set; }
[JsonProperty("lines")]
public List<SubwayLine> Lines { get; set; }
}
[JsonProperty("name")]
标记指定了与类的该属性相对应的 JSON 对象属性。该段代码与上一节中给出的部分 JSON 字符串相对应。
五、与性能测试相关的说明
在作业的要求文档中有与性能测试与改进相关的博客内容建议,但本项目没有进行,主要基于以下两个考虑:
- 北京地铁地图的数据规模只有 量级,且相关算法的时空复杂度又几乎是线性。在这样的情况下,首先算法本身效率已经接近上界,不具备较大的提升空间了;其次在这个规模下算法的时间开销几乎可以忽略不计,也没有花费时间成本打磨的必要;
- 根据作业文档的需求,本项目的应用场景主要在于供使用者进行日常查询,而非自动化、批处理化的计算、输出大量指定数据。在这样的单用户查询应用场景下,程序运行到得出结果之间的时间消耗完全不会影响用户体验,在用户的角度目前程序的结果已经近乎即时显现了,具体至毫秒的性能测试和毫秒级别的性能改进对于改善用户体验来说没有太大的意义。
六、对扩展需求的说明
作业文档中提出了两个可进行二选一扩展需求,本项目选择完成的是第二个,即:
让程序能处理上海的地铁地图,或者其他城市的地图。把程序由”固定处理一个地图“升级为”能处理多个地图“,程序的什么模块需要变化?
本项目实际已经基本完成了这个需求,整个软件除北京地铁地图外,没有任何与具体城市、具体地图相关的数据硬编码进软件,地铁系统的地铁站、地铁线路等各种数据都是由 JSON 文件提供的,而且项目设计并实现的各种算法、命令参数等也与具体的城市无关,这也就意味着如果需要更换至另外一个城市的地图,只需要更换新城市的 JSON 数据文件、新城市的地铁线路图即可,程序本身不需要做出改变。