目录
零、分工
一、GitHub地址
二、PSP表格
三、控制台程序解题思路
- 建模思路和文本信息
- Dijkstra算法——/b功能
- /c功能实现
- /a功能实现
- 换乘优化
- /z测试功能实现
四、控制台程序实现
- Dijkstra算法——/b功能代码
- /a功能代码
- /z测试功能代码
五、控制台程序性能分析 六、控制台黑盒测试
1./b功能
2./a功能 部分结果
3./c功能
4./z测试功能
七、界面设计
1.界面设计思路
2.将C++封装为dll
3.图形界面的实现
4.绘图功能的实现
5.界面功能基本完成
八、个人感悟
零、分工
控制台:张玉伟
GUI: 徐志涛
一、GitHub地址
github地址:https://github.com/ZhangWuren/SE-Team-Project
二、PSP表格
三、控制台程序解题思路
1. 建模思路和文本信息
在本项目中,建模的对象是北京地铁图,在建模的过程中,建立了两个类,分别为类Station和类Map。类Map包含了类Station,具体的类图在项目完成后展示。
在文本数据beijing-subway.txt中,记录了地铁站及其对应的线路,举例如下
1 苹果园
1 古城
1 八角游乐园
1 八宝山
1 玉泉路
1 五棵松
1 万寿路
1 公主坟-10
1 军事博物馆-9
1 木樨地
1 南礼士路
1 复兴门-2
1 西单-4
1 天安门西
1 天安门东
首列为当前线路,接着是对应的站名,-表示当前车站为换乘车站,跟着的是换乘线路,多个换乘线路用“,”隔开
经统计,北京市地铁最新的不重复地铁站数量为330。
2. Dijkstra算法——/b功能
在/b功能的实现过程中,将问题抽象出来,就是一个无向图最短路问题,整个地铁图为无向图,寻找两点之间站点最少的路线。很容易想到的就是曾经学过的Dijkstra算法。在Dijkstra算法中,重要的是设置邻接矩阵。
3. /c功能实现
/c功能是输出整条线路,只需要简单的输出同一条线路上的地铁站即可。
4. /a功能实现
/a功能是从选定站点出发,遍历全部的站点。在思考全遍历功能时,想起了之前相似的问题有tsp问题,哈密顿回路,欧拉回路等。其中tsp问题要求每个节点只能访问一次,和哈密顿回路类似。欧拉回路则是要求每个边都访问一次。这里的全遍历功能和它们都不太一样,本题强调要遍历所有站点,对于边并不做要求,只要点遍历到了,有的边甚至是可以忽略的,同时每个点的访问次数也可以是多次的。
经过在网络上搜索相关资料,决定采用贪心策略+dijkstra算法,从给定起点start开始,随机选取一个没有路过的节点作为end,用之前/b功能的dijkstra算法找到这两个点之间的最短路。接着将end作为新的start,再选取没有经历的节点最后end,直到所有点都遍历结束,最后再返回给定的初始start。在这个过程中,dijkstra算法的结果是局部最优解,运用贪心的策略,可以判定整个遍历的过程也可取到最优的解。
5. 换乘优化
题目中要求,换乘时因为各种原因,相当于坐了三个站。一开始我陷入了难题,因为每个站点都是独立的,譬如“公主坟”这个换乘站,公主坟既在1号线,又在10号线,在不考虑换乘为3站的情况下,它和“万寿路”军事博物馆“”莲花台“西钓鱼台”这四个站点的邻接矩阵权值都是1,但是考虑换乘优化时,权值要根据上一站点变化,陷入了僵局。
(所说权值均表示邻接矩阵权值)后来经过很长时间的思考,就是设置多个同名的换乘站表示不同地铁线上的同一个换成站,同名换乘站之间权值为0,同线路前后站权值为1,不同线路前后站权值为3。用“公主坟”举例,即创建一个1号线上的公主坟,一个10号线上的公主坟,1号线上的公主坟和“万寿路”军事博物馆“权值为1,和”莲花台“西钓鱼台”权值为3表示换乘,和10号线上的公主坟权值为0,因为同个站点本质上是在一起的,能够成功的解决换乘优化问题。
6./z测试功能实现
/z功能也比较简单,首先根据设定好的邻接矩阵来判断遍历顺序是否合理。然后再判断节点数以及是否全部遍历,再给出遗漏的站点,比较简单。
四、控制台程序实现
1. Dijkstra算法——/b功能代码
Dijkstra算法的另一块重点部分为邻接矩阵权值的设定,代码如下
2. /a功能代码
其中函数setTransMartix()为换乘优化时的矩阵,具体如下
void Map::setTransMartix()
{
memset(m_maze, INF, sizeof(m_maze));
//2、10为环线
m_maze[23][38] = 1;
m_maze[38][23] = 1;
m_maze[173][207] = 1;
m_maze[207][173] = 1;
for (int i = 0; i < _TOTALS; i++)
{
if (i != 0 && i != _TOTALS - 1)//如果不是第一个和最后一个
{
if (stations[i].getType())
{//如果是换乘车站,则需要考虑换乘的代价
{//本条线路上权值仍未1
if (stations[i].compareLine(stations[i + 1])) {
m_maze[i][i + 1] = 1;
}
if (stations[i].compareLine(stations[i - 1])) {
m_maze[i][i - 1] = 1;
}
}
{//别的线路上可以换乘的站点
for (int j = 0; j < _TOTALS; j++)
{
if (stations[i].getNumber() == stations[j].getNumber() && i != j)
{//找到除了本身以外的同名站点
m_maze[i][j] = 0;//同名站点可以直接到达,赋权值为0
//和同名站点的相邻站点,即为换乘,赋权值为3
if (j != 392)
{//需考虑巴沟站没有j+1站
if (stations[j].compareLine(stations[j + 1])) {
m_maze[i][j + 1] = 3;
}
if (stations[j].compareLine(stations[j - 1])) {
m_maze[i][j - 1] = 3;
}
}
else
{
m_maze[i][j - 1] = 3;
}
}
}
}
}
else
{//如果不是换乘车站,直接将权值赋为1
if (stations[i].compareLine(stations[i + 1])) {
m_maze[i][i + 1] = 1;
}
if (stations[i].compareLine(stations[i - 1])) {
m_maze[i][i - 1] = 1;
}
}
}
else
{
if (i == 0)
{
m_maze[i][i + 1] = 1;
}
else
{
m_maze[i][i - 1] = 1;
m_maze[i][195] = 3;
}
}
}
}
3./z测试功能代码
void Map::test(string filename)
{
this->setMartix();
memset(this->m_tvis, false, sizeof(m_tvis));
fstream fin(filename);
string readline;
string stas[_TOTALS * 10];
for (int i = 0; i < _TOTALS * 10; i++)
{
stas[i].clear();
}
getline(fin, readline);
int count = stoi(readline);
int count1 = 0;
int i = 0;
while (getline(fin, readline))
{
stas[i] = readline;
i++;
}
for (int i = 1; !stas[i].empty(); i++)
{
Station s1 = this->getStationbyname(stas[i]);
Station s2 = this->getStationbyname(stas[i - 1]);
m_tvis[s1.getNumber()] = true;
m_tvis[s2.getNumber()] = true;
if (!this->m_maze[s1.getNumber()][s2.getNumber()])
{
cout << "error" << endl;
return;
}
}
int novissta[_TOTAL];
memset(novissta, -1, sizeof(novissta));
for (int i = 0, j = 0; i < _TOTAL; i++)
{
if (this->m_tvis[i] == false)
{
novissta[j] = i;
j++;
}
}
if (novissta[0] == -1)
{
cout << "true" << endl;
}
else
{
cout << "false" << endl;
cout << "遗漏的站点有:" << endl;
for (int i = 0; novissta[i] != -1; i++)
{
cout << this->getStationbynumber(novissta[i]).getName() << endl;
}
}
}
五、控制台程序性能分析
对全遍历功能进行性能分析,发现耗时较长的函数是全遍历函数traversal以及其调用的贪心迪杰斯特拉搜索greedSearch,均控制在较短时间,不再做进一步优化。
还有读取地图的setMap函数,本项目采用txt方式,逐行读取北京地铁图,因为数据量较小,故不做进一步改进。
六、控制台黑盒测试
1./b功能
和百度地图比较
基本类似
2./a功能 部分结果
3./c功能
房山线
4./z测试功能
将/a的结果用作测试时,应该为true
删除部分/a的结果
造成错误站点
七、界面设计
1.界面设计思路
以C++代码为功能,将函数封装为dll供图形界面调用,因为在小学期的时候用过C#写过图形界面,所以决定采用C#构建图形界面,在C++函数中将要走过的站点依次输出到文本文件中,然后再根据之前存储的坐标信息将各个站点的位置和路线画在以北京地铁线路图为背景的画布上。
2.将C++封装为dll
C++函数封装成动态链接库时才能提供接口供C#进行调用,首先创建一个dll项目,然后将之前编写好的C++头文件复制到这个项目中,再将要用到的函数声明在dll项目的头文件中,如下图所示
之后在C#中就可以直接声明调用函数了
项目目录大致是这样的,然后再将项目进行编译 ,项目目录下会生成一个名字为Subway_dll.dll的动态链接库文件,我们这时就的到想要的dll文件,然后将这个dll文件复制到C#图形界面项目的可执行文件目录下,就能在图形界面进行C++函数的调用了
3.图形界面的实现
在窗体中导入北京地铁线路图,想要正确的绘图首先要得到各个站点的坐标,然后为了得到各个点的坐标,我写了一个Onclick事件,每点一次就获得当前位置的坐标,将各个站点的坐标依次记录在Beijing_Subway_Location.txt文件中。然后就可以根据经过的站点信息画图了。
为了能够在C#项目中调用遍历功能的C++函数,需要再主函数这里引入声明要调用的函数
这个函数会将经过的站点信息依次输出存入txt文件中,然后C#读这个文件,进行绘图。
4.绘图功能的实现
因为在绘制路线图的过程中会反复进行绘图,所以编写了一个DrawTool类来实现绘图功能,这个类中会有两个功能,一个是画图,一个是画带箭头的线来表明路线,代码如下
5.界面功能基本完成
设置了一个Button_Click事件,点击Search按钮即可实现绘制线路图,点击Search先会调用ResetMap函数来刷新界面,防止上一次的绘图对本次绘图发生影响,具体调用绘图功能的代码如下
执行如下命令
界面如图所示,手动输入坐标会有误差,站点绘制的并不是非常准确,在可以接受的误差范围内
八、个人感悟
本次结对项目中我负责界面的编写,学习了运用C#写图形界面以及一些C#的语法,了解了dll的用法,也学习了如何合作使用GitHub,本次项目收获颇丰