目录
零、分工
控制台:张玉伟
GUI: 徐志涛
一、GitHub地址
github地址:https://github.com/ZhangWuren/SE-Team-Project
二、PSP表格
PSP | 过程 | 预估耗时 (分钟) | 实际耗时 (分钟) |
Planning | 计划 | 10 | |
·Esitimate | ·估计耗时 | 10 | 15 |
Development | 开发 | 1020 | |
·Analysis | ·需求分析 | 30 | 120 |
·Design Spec | ·生成设计文档 | 120 | 200 |
·Design Review | ·设计复审 | 30 | 20 |
·Coding Standard | ·代码规范 | 30 | 60 |
·Design | ·具体设计 | 150 | 120 |
·Coding | ·编码 | 300 | 2400 |
·Code Review | ·代码复审 | 60 | 30 |
·Test | ·测试 | 300 | 100 |
Reporting | 报告 | 130 | |
·Test Repor | ·测试报告 | 60 | 30 |
·Size Measurement | ·计算工作量 | 10 | 30 |
·Postmortem & Process Improvement Plan | ·总结,改进计划 | 60 | 30 |
总计 | 1160 | 3155 |
三、控制台程序解题思路
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功能代码
void Map::search(string start, string end) //dijkstra算法
{
..
m_dis[startnumber] = 0;
for (int i = 0; i < _TOTAL; i++)
{
//找到和起点距离最短的点
int minx = INF;
int minmark;
for (int j = 0; j < _TOTAL; j++)
{
if (m_vis[j] == false && m_dis[j] <= minx)
{
minx = m_dis[j];
minmark = j;
}
}
//并标记
m_vis[minmark] = true;
//更新所有和它连接的点的距离
for (int j = 0; j < _TOTAL; j++)
{
if (m_vis[j] == false && m_dis[j] > m_dis[minmark] + m_maze[minmark][j])
{
m_dis[j] = m_dis[minmark] + m_maze[minmark][j];
m_path[j] = minmark;
}
}
}
this->printPath(endnumber, lastline);
}
Dijkstra算法的另一块重点部分为邻接矩阵权值的设定,代码如下
void Map::setMartix()
{
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].compareLine(stations[i + 1])) {//判断是否为同一条线路
m_maze[stations[i].getNumber()][stations[i + 1].getNumber()] = 1;
}
if (stations[i].compareLine(stations[i - 1])) {
m_maze[stations[i].getNumber()][stations[i - 1].getNumber()] = 1;
}
}
else
{
if (i == 0)
{
m_maze[stations[i].getNumber()][stations[i + 1].getNumber()] = 1;
}
else
{
m_maze[stations[i].getNumber()][stations[i - 1].getNumber()] = 1;
}
}
}
}
2. /a功能代码
void Map::traversal(string start)
{
this->setTransMartix();
..
m_tvis[startsta.getNumber()] = true;
while (visnumber > 0)
{
for (int i = 0; i < _TOTAL; i++)
{
if (this->m_tvis[i] != true)
{
Station endsta = this->getStationbynumber(i);
this->greedSearch(startsta, endsta);
startsta = endsta;
visnumber = this->getRemainStationNumber();
break;
}
}
}
this->greedSearch(startsta, this->getStationbyname(start));
..
}
其中函数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类来实现绘图功能,这个类中会有两个功能,一个是画图,一个是画带箭头的线来表明路线,代码如下
public static class DrawTool
{
/// <summary>
/// 线画笔颜色,在构造函数中初始化何种颜色
/// </summary>
public static Color line_brush_color = new Color();
/// <summary>
/// 圈画笔颜色,在构造函数中初始化何种颜色
/// </summary>
public static Color circle_brush_color = new Color();
static DrawTool()
{
DrawTool.line_brush_color = Color.FromArgb(0, 0, 0);
DrawTool.circle_brush_color = Color.FromArgb(0, 0, 255);
}
/// <summary>
/// 绘制指定颜色粗细带箭头的线
/// </summary>
public static void DrawArrowLine(Graphics g, float x1, float y1, float x2, float y2, float width)
{
Pen p = new Pen(DrawTool.line_brush_color, width);
p.EndCap = LineCap.ArrowAnchor; // 定义线尾的样式为箭头
g.DrawLine(p, x1, y1, x2, y2);
}
/// <summary>
/// 绘制圆心(x,y),半径r,宽度为width的空心圆
/// </summary>
public static void DrawCircle(Graphics g, float x, float y, float r, float width)
{
Pen p = new Pen(DrawTool.circle_brush_color, width);
g.DrawEllipse(p, (int)(x - r), (int)(y - r), (int)(2 * r), (int)(2 * r));
}
}
5.界面功能基本完成
设置了一个Button_Click事件,点击Search按钮即可实现绘制线路图,点击Search先会调用ResetMap函数来刷新界面,防止上一次的绘图对本次绘图发生影响,具体调用绘图功能的代码如下
public void ResetMap()
{
Bitmap bitmap = new Bitmap("subway_map.bmp");
Rectangle r = new Rectangle(0, 0,this.pictureBox1.Size.Width, this.pictureBox1.Size.Height);
Form1.graphics.DrawImage(bitmap, r); // 使用原地图覆盖
}
StreamReader sr = new StreamReader("subway_station.txt", Encoding.Default);
String readline;
float x1 = 0, x2 = 0, y1 = 0, y2 = 0;
int flag_first = 0;
int index = 0;
while ((readline = sr.ReadLine()) != null)
{
x2 = 0;
y2 = 0;
index = 0;
while (readline[index] != ' ')
{
x2 = x2 * 10 + readline[index] - '0';
index++;
}
index++;
while (index < readline.Length)
{
y2 = y2 * 10 + readline[index] - '0';
index++;
}
DrawTool.DrawCircle(graphics, x2, y2, 8,2);
if (flag_first == 0) flag_first = 1;
else
{//画从x1,y1到x2,y2的线
DrawTool.DrawArrowLine(graphics, x1, y1, x2, y2, 2);
}
x1 = x2;
y1 = y2;
}
执行如下命令
界面如图所示,手动输入坐标会有误差,站点绘制的并不是非常准确,在可以接受的误差范围内
八、个人感悟
张玉伟:在本次结对项目中,我负责控制台程序的编写,运用了曾经学过的迪杰斯特拉算法,对地铁图的建模思想也有所收获,同时对多人共同使用github进行团队合作,收获颇多。