一、项目地址
我们小组两人功用一个代码仓库,github地址为:https://github.com/ZJT1024/subway
二、各模块开发时间预估
注:实际耗时在结尾处给出。
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
---|---|---|
Planning | 计划 | 20 |
Estimate | 估计这个任务需要多少时间 | 30 |
Development | 开发 | 1440 |
Analysis | 需求分析(包括学习新技术) | 60 |
Design Review | 生成设计文档 | 120 |
Coding Standard | 代码规范(为目前的开发指定合适的规范) | 60 |
Design | 具体设计 | 240 |
Coding | 具体编码 | 2160 |
Code Review | 代码复审 | 120 |
Test | 测试(自我测试,修改代码,提交修改) | 120 |
Reporting | 报告 | 90 |
Test Report | 测试报告 | 90 |
Size Measurement | 计算工作量 | 30 |
Postmortem & Process Improvement Plan | 事后总结,并提出修改过程计划 | 30 |
合计 | 4610 |
三、学习过程、解题思路
本次项目难度较个人项目而言,有所增加,不仅要求编写用户界面,而且开发过程开始向团队化过度。通过该学期对面向对象设计和编程方法的学习,本次项目,我们小组尝试使用面向对象的方式进行项目开发。
3.1开发语言及运行环境
因为本次项目要求中地铁站定数量只有300多个,对程序而言,能后很快的运行,所以,根据我们小组两名队员的商议,决定使用C++编写搜索和遍历算法,用python编写UI界面和并实现输入输出。
3.2面向对象分析与设计
在本次项目开发过程中,我主要负责编写两点间最短距离和全局遍历,因此,对该部分的过程掌握的比较清晰。通过对项目建立功能模型、静态模型和动态模型,全方位的运用面向对象的分析方式对系统进行分析。
3.2.1 建立功能模型(用例模型)
根据使用百度地图、高德地图的软件的经验,我们小组两名成员经过讨论,确定了系统的参与者、系统用例、和用例间的关系,讨论结果如下:
参与者:普通用户、管理员
系统用例:
用例名称 | 用例功能 | 用例间关系 |
---|---|---|
查询地铁线路 | 接用户输入的地铁线路名称,并反馈该条线路上的站点名称序列 | |
查询两站之间最短路 | 根据用户输入的两个站点信息和选择的路线求解方式进行计算,返回两个站点之间的最短路径(路径由一系列站点名称和换成线路构成) | 和“打表记录”用例相结合,可以实现地铁的全遍历 |
打表记录 | 系统的辅助功能,帮助记录已经探索过的结点 | 和“查询两站之间最短路”搭配使用 |
全遍历 | 接收用户输入的站点信息,以该站点为起点,遍历整个地铁网络 | 多次调用 “查询两点之间最短路”得到 |
测试 | 根据管理员输入的测试信息和文件对系统进行测试 | 调用“查询两点之间最短路”用例 |
跟新数据库 | 管理员可对系统中的地铁线路数据进行根性 |
系统的用例图如下:
3.2.1 建立静态模型(对象模型)
根据地铁系统的特点,我们小组两名成员经过讨论,确立了一下三个类,分别是站点类(class Station)、地铁线类(class Line)、地铁系统类(class System),他们的属性如下:
3.2.3 建立动态模型
顺序图:
状态图:
3.3解题思路
3.3.1 用Dijkstra实现两点最短路
题目要求计算两点间的最短路径,想了很多创新的想法,比如先判断两个站点是否在同一地铁线路上,是否需要换乘以及怎样快速计算有换乘车站时的最少站点数等。但是为了减少后期隐含bug的可能性,在此还是决定使用经典的单元最短路算法,即Dijkstra算法。(参考资料:Dijkstra算法)
核心代码如下:
// 寻找从当前节点开始能到达的站点的最短路
for (int i = 0; i < graph[now.first].size(); i++)
{
to = graph[now.first][i];
if (visited[to.first]) // 打表记录已经访问过的站点
{
continue;
}
else
{
int cost = distance[now.first] + 1; // 默认每条路径的权值为1
if (path[now.first].second != 0 && path[now.first].second != to.second)
{
// 判断是否需要换成,如果需要的话,消耗额外的精力
cost += extral_cost * transform;
}
if (distance[to.first] > cost)
{
distance[to.first] = cost; // 更新最短路
que.push(to); // 将该站点加入队列,继续扩展
path[to.first] = make_pair(now.first, to.second); // 记录路径信息,便于之后输出站点序列
// 判断并记录当前站点是否是换成站点
if (path[now.first].second == 0)
{
transform_station[to.first] = 0;
}
else if (path[now.first].second == to.second)
{
transform_station[to.first] = 0;
}
else
{
transform_station[to.first] = to.second;
}
if (to.first == eid)
{
flag = 1;
break;
}
}
}
}
在上述的代码段中,使用了优先队列que对扩充站点信息进行排序,目的是降低Dijkstra的算法复杂度。
3.3.2用遍历实现全图搜索
全遍历部分,题目要求用户输入一个站点名称作为起点,要求遍历整个地铁网络,最后回到起点,该算法的难点在于,每个站点是可以重复计数的,而且,由于地铁线路特征,有的站点必须重复计数,这就是的已有的哈密顿算法不在适用。在网上学习,看到好多大佬适用欧拉回路算法,但是,经过小组两名成员的探讨,题目要求遍历所有站点,而不必遍历所有边,因此觉得适用欧拉回路算法也不太合适。加上最短路径遍历是一个NP难题,考虑到站点数量不多,因此在此决定使用直接暴力的遍历搜索。
大致想法为:用一个集合记录未访问过的站点,每次从集合中取出一个站点信息作为终点,从当前所在站点出发,利用上一步编写的Dijkstra算法计算两点间的最短路。重复上述步骤,直到所有站点被遍历完成后,从最后一个站点以最短路回到起点。
核心代码如下:
while (!station_que.empty()) // station_que记录所有站点,取出不放回
{
int to = station_que.front();
station_que.pop();
if (station_book[to]) // 根据打表的记录判断当前站点是已经访问过
{
continue;
}
output = "";
string start_station = decode_station[now], end_station = decode_station[to];
cost += Find_the_route(start_station, end_station, 0, output); // 多次调用Dijkstra求解两点间的最短路
cost--;// 起始结点多算了一次
station_book[to] = 1;
order += output;
now = to; // 当前所在站点为起点,继续扩展
}
output = "";
cost += Find_the_route(decode_station[now], now_station, 0, output);
order += output;
该部分的代码还可以继续优化,因为用station_que记录的顺序是固定的,所以没有做到最优,如果优化,可以使用优先队列,将为访问的站点按照距离当前站点的路径大小由小到大排序,但是这样有需要损失一定的时间去计算到该站点最近的下一个站点的信息爱,因此优化是还要注意取舍。
四、设计实现过程
4.1 程序流程图
4.2 程序类图(后端c++)
4.3 UI界面编写和参数传递(前段python)
使用的库
名字 | 作用 |
---|---|
xml.etree.cElementTree | 用来解析存储在文件中的地铁信息 |
subwaystation | 自定义的类,用来保存每一个地铁站的信息 |
tkinter | pythonUI库 |
codecs | python用来进行文件读取的库 |
time | 对进程进行暂停 |
os | 完成python对C/C++可执行文件的调用 |
主要数据结构
名字 | 存储内容 |
---|---|
line_dic | 用来存储线路的信息,其中线路的名字作为索引 |
station_list | 每一个元素都存储着一个车站的信息 |
transfer_station_list | 每一个元素存储着一个换乘车站的信息 |
station_label_list | 以二元元组的形式存储着车站和其所对应的label |
主要函数
名字 | 功能 | 参数 | 返回值 |
---|---|---|---|
decode_xml | 将xml文件中存储的地铁信息解析 | 无 | 无 |
find_a_way_by_cost | 根据花费最少寻找站点A到站点B的最短路径 | 无 | 无 |
show_line | 按照输入动态显示这条路线的车站 | 无 | 无 |
find_a_way_by_effort | 根据精力最少寻找站点A到站点B的最短路径 | 无 | 无 |
find_a_way_by_effort | 根据精力最少寻找站点A到站点B的最短路径 | 无 | 无 |
search_a_way | 寻找从本地出发返回本地的最短路径 | 无 | 无 |
五、程序性能分析
5.1 两个站点之间的最短路
使用优先队列对Dijkstra进行优化,代码质量得到提高。
5.1 全遍历
由于需要多次调用Find_the_route函数求解两个站点间的最短路,导致时间有些浪费。
六、单元测试
由于系统的最核心算法为两点间的最短路径算法,其他算法都依靠调用该算法得以实现,因此,在此仅对求最短路径的算法进行单元测试。测试使用的是黑盒测试,共进行了12次测试,大致对极限指令、错误指令、有效指令都进行了测试,测试结果如下:
七、结果展示
7.1 初始界面
7.2 查询某一条地铁线路
7.3 查询两点间最短路径
7.4 全遍历
上图中,从香山出发,开始遍历,红色结点表示重复经过的站点,白色结点表示只进过一次的站点。
八、模块实际开发时间及与预期对照
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 30 |
Estimate | 估计这个任务需要多少时间 | 30 | 20 |
Development | 开发 | 1440 | 720 |
Analysis | 需求分析(包括学习新技术) | 60 | 180 |
Design Review | 生成设计文档 | 120 | 90 |
Coding Standard | 代码规范(为目前的开发指定合适的规范) | 60 | 30 |
Design | 具体设计 | 240 | 240 |
Coding | 具体编码 | 2160 | 1560 |
Code Review | 代码复审 | 120 | 90 |
Test | 测试(自我测试,修改代码,提交修改) | 120 | 100 |
Reporting | 报告 | 90 | 90 |
Test Report | 测试报告 | 90 | 60 |
Size Measurement | 计算工作量 | 30 | 20 |
Postmortem & Process Improvement Plan | 事后总结,并提出修改过程计划 | 30 | 20 |
合计 | 4610 | 3250 |
九、个人总结、心得体会
这次项目是我为数不多的几次团队合作项目中的一次,加上尝试了使用不用语言之间的相互调用,对我个人来说是一个挑战,不过,好在有队友的支持和冷静分析,很多bug解决起来都比预想的顺利得多。这也让我充分意识到,一个人编写代码时往往会或略很多瑕疵,而多个人共同完成项目就有效的起到了监督的作用。另外,我再一次深刻的认识到了沟通的重要性!
在不同语言间相互调用方面,我们采用了最简单的用文件传递数据,就省去了很多动态链接库的编写内容,这也让我们充分的认识到了程序至今异步调度的方式方法。
缺陷和可提升空间
1.本次带UI的任务界面考虑到MVC分离的方式,但是并没有真正实施,tkinter的参考资料有限。
2.UI设计太不美观:本次作业学习时间有限,下次碰到类似任务会优先考虑PyQt等成熟的UI库来完成设计。