本课题采用C++语言,利用图形化工具Qt5.8.0设计,完整代码在 github上,请下载后查看。除代码外,github上的资源还包括:
- 含有可独立运行的exe可执行文件的Navigate文件夹
- 程序可能用到的原始数据
课题内容和要求
根据南京邮电大学仙林校区的校内主要建筑物和道路,构建一张带权无向图。完成校内导航模式设计和游览模式设计。具体要求如下:
(1)构建校内带权无向图,并用图形界面进行显示。
(2)导航模块设计:输入起始位置和终点位置,给出最优路线及两条备选路线。
(3)游览模式设计:构建算法,分析是否能设计遍览校内景点一次且仅一次的路线图和遍历校内所有路径一次且仅一次的路线图。
课题需求分析
本课题目标系统“校园导航、游览模式设计”的功能框架图如图1所示。
(1)支持选择起点和终点,并给出最短路径。
(2)给出两条备选路径,并用不同颜色显示;为方便区别,给出备选路径的时候清空推荐路径。
(3)设计一次游园模式,即从一个顶点出发,能遍历所有的点一次且仅一次。
(4)支持地图选点,查看详细信息,并设置为起点或终点。
(5)设计一次遍历所有路径一次且仅一次模式,若不能遍历完所有路径,则转到游园模式。
相关数据结构及算法设计
主要的数据结构
采用邻接表存储带权无向图的信息,数据结构如下:
typedef struct ENode{
int adjVex;//任意顶点u相邻接的顶点
int w;//边的权值
struct ENode* nextArc;//指向下一个边结点
}ENode;
typedef struct{
int n;//图的当前顶点数
int e;//图的当前边数
ENode **a;//指向一维指针的数组
}LGraph;
主要算法流程
(1)路径规划算法:使用Dijkstra算法求最短路径,先初始化数组s,d,path,然后求最短路径,更新d和path并把顶点k加入s中,循环再求最短路径。其算法的流程图如下:
(2)备选路径算法:求备选路径时,先将Dijkstra算法求出的最短路径存入isChosen数组中,然后删除数组中权值最大的边,再用Dijkstra算法求最短路径,此时求得第一条备选路径。再从备选路径数组中删除权值最小的边,用Dijkstra算法求最短路径,此时求得第二条备选路径。
(3)设计一次游园路径:利用图的深度优先遍历,从一个顶点出发,调用递归算法,求能否经过所有的点一次且仅一次。其算法流程图如下:
(4)设计一次遍历所有的路:从某一结点出发,遍历所有的路一次且仅一次,即判断图是不是“欧拉(半)图”。对于连通图来说,如果结点没有奇数度节点,则其是欧拉图;若有两个奇数度节点,则其是欧拉半图。因此只要数奇数度节点即可判断能不能遍历所有的路一次且仅一次。
(5)路径展示算法:根据路径规划算法中求得的结点序列,在背景图上依次连接各个结点。
(6)地图选点:捕捉鼠标左键单击位置,判断其与图上点的位置关系,以此来区分不同的点。
源程序代码 (部分)
- map.h定义Dijkstra算法,用于求最短路径
void Dijkstra(int v);//dijkstra算法求最短路径
map.cpp实现如下:
void Map::Dijkstra(int v)
{
ENode *q;
if (v<0 || v>lg->n)
return ;
int *s = (int *)malloc(sizeof(int)*lg->n);
int i;
ENode *p;
for (i = 0; i < lg->n; i++) //初始化
{
d[i] = INFTY;
s[i] = 0;
path[i] = -1;
}
p=lg->a[v];
while (p) //顶点v为源点
{
d[p->adjVex] = p->w;
if (i != v && d[p->adjVex] < INFTY)
path[p->adjVex] = v;
p = p->nextArc;
}
s[v] = 1;
d[v] = 0;
int k;
for (i = 1; i <= lg->n-1; i++){
k = Choose(d, s, lg->n);
s[k] = 1; //k加入s中
q = lg->a[k];
while (q) //更新d和path
{
if (s[q->adjVex] == 0 && d[k] + q->w < d[q->adjVex]){
d[q->adjVex] = d[k] + q->w;
path[q->adjVex] = k;
}
q = q->nextArc;
}
}
free(s);
}
2)map.h定义SecondDijkstra算法,用于求备选路径
void SecondDijkstra(int u, int v); //dijkstra算法求第二路径
map.cpp实现如下:
void Map::SecondDijkstra(int u,int v)//删除权值最大的边,重新求最短路径
{
int k=0;
ENode *p,*q;
int t=0;
int maxEdge=0;
for(int i=0;i<lg->n;i++) {
p=lg->a[i];
while(p){
if(maxEdge<=p->w&&isChosen[i][p->adjVex]==true&&OutEdge(i)>1&&OutEdge(p->adjVex)>1){
maxEdge=p->w;
k=i;
t=p->adjVex;
}
p=p->nextArc;
}
}
Remove(k,t);
Remove(t,k);
if (v<0 || v>lg->n)
return;
int *s = (int *)malloc(sizeof(int)*lg->n);
int i;
for (i = 0; i < lg->n; i++) //初始化
{
Secondd[i] = INFTY;
s[i] = 0;
SecondPath[i] = -1;
}
p = lg->a[u];
while (p) //顶点u为源点
{
Secondd[p->adjVex] = p->w;
if (i != u && Secondd[p->adjVex] < INFTY)
SecondPath[p->adjVex] = u;
p = p->nextArc;
}
s[u] = 1;
Secondd[u] = 0;
for (i = 1; i <= lg->n-1; i++){
k = Choose(Secondd, s, lg->n);
s[k] = 1; //k加入s中
q = lg->a[k];
while (q) //更新d和path
{
if (s[q->adjVex] == 0 && Secondd[k] + q->w < Secondd[q->adjVex]){
Secondd[q->adjVex] = Secondd[k] + q->w;
SecondPath[q->adjVex] = k;
}
q = q->nextArc;
}
}
free(s);
//选择矩阵归零,以备一下次选择
for(int i=0;i<Max_Vex;i++)
for(int j=0;j<Max_Vex;j++)
isChosen[i][j]=false;
}
3)map.h定义欧拉(半)图的判断和输出:
int JudgeEulur(); //判断欧拉图
void DFSEdge(int v); //深度优先遍历边
map.cpp实现如下:
int Map::JudgeEulur()
{
int count=0;
for(int i=0;i<Max_Vex;i++){
if(OutEdge(i)%2==1) //数奇数度节点
count++;
}
return count;
}
void Map::DFSEdge(int v)
{
DFSedgevisited.push_back(v);
ENode*w;
int q;
for(w=lg->a[v];w;w=w->nextArc){
q=w->adjVex;
if(Exist(v,q)){
Remove(v,q);
Remove(q,v);
DFSEdge(q);
}
}
}
4)mainwindow.h定义FindPath()函数,用于找到最短路径并在地图上画出来。
void FindPath(); //用于寻找最短路径并在图上显示,响应开始导航按钮和切换路线按钮
mainwindow.cpp实现如下:
void MainWindow::FindPath()
{
page=0;
Clear();
AddEdge();
QPainter paint(&paintMap->map);
m->Dijkstra(StartComboBox->currentIndex());
QVector<int> nextPath;
nextPath=get_Path(EndComboBox->currentIndex());
paint.setBrush(Qt::red);
paint.drawEllipse(paintMap->qpointF[nextPath[0]]-QPointF(50,150),5,5);
paint.setBrush(Qt::blue);
paint.drawEllipse(paintMap->qpointF[nextPath[nextPath.size()-1]]-QPointF(50,150),5,5);
QPen pen;
pen.setWidth(3);
pen.setColor(QColor(50,205,50));
paint.setPen(pen);
for(int i=1;i<nextPath.size();i++){
qDebug()<<nextPath[i]<<",";
m->setIsChosen(nextPath[i-1],nextPath[i],true);
}
//开始画路径
QPainterPath path;
path.moveTo(StartPos.x()-50,StartPos.y()-150);
for(int i=1;i<nextPath.size();i++){
path.lineTo(paintMap->qpointF[nextPath[i]].x()-50,paintMap->qpointF[nextPath[i]].y()-150);
paint.drawPath(path);
path.moveTo(paintMap->qpointF[nextPath[i]].x()-50,paintMap->qpointF[nextPath[i]].y()-150);
}
this->update(); //调用PaintEvent重画事件
page++;
}
5)mainwindow.h定义函数PaintEvent
void paintEvent(QPaintEvent *);//重画事件
mainwindow.cpp重新实现此虚函数如下:
void MainWindow::paintEvent(QPaintEvent *event)
{
QPainter paint(this);
paintMap->drawBackground(&paint);//画背景地图
//画图例
paint.setBrush(Qt::red);
paint.drawEllipse(QPointF(625,55),5,5);
paint.drawText(QPointF(635,60),tr("起点"));
paint.setBrush(Qt::blue);
paint.drawEllipse(QPointF(625,75),5,5);
paint.drawText(QPointF(635,80),tr("终点"));
QPen pen;
pen.setWidth(3.5);
pen.setColor(QColor(50,205,50));
paint.setPen(pen);
paint.drawLine(QPointF(600,95),QPointF(625,95));
pen.setColor(Qt::black);
paint.setPen(pen);
paint.drawText(QPointF(635,100),tr("推荐路线"));
pen.setWidth(3.5);
pen.setColor(QColor(173,255,47));
paint.setPen(pen);
paint.drawLine(QPointF(600,115),QPointF(625,115));
pen.setColor(Qt::black);
paint.setPen(pen);
paint.drawText(QPointF(635,120),tr("备选路线1"));
pen.setWidth(3.5);
pen.setColor(QColor(144,238,144));
paint.setPen(pen);
paint.drawLine(QPointF(600,135),QPointF(625,135));
pen.setColor(Qt::black);
paint.setPen(pen);
paint.drawText(QPointF(635,140),tr("备选路线2"));
}
6)mainwindow.cpp构造函数中,实现关联切换路径与函数的代码如下:
connect(ReverseButton,&QPushButton::released,
[=]()mutable{
if(page%3==1){
emit FindSecondPath();
page++;
}else if(page%3==2){
emit FindThirdPath();
page++;
}else{
emit FindPath();
}
});
程序所有的源代码已上传github:
https://github.com/FFtu-ChrisKun/Navigate_in_NJUPT
测试数据及其结果
1)测试导航模块:
测试用例1:起点:南门 终点:北门
测试用例2:起点:教2 终点:桂苑
测试结果:
2) 测试地图选点:
测试用例:起点:青教 终点:南一
测试结果:
3)测试切换路线:
测试用例:起点:青教 终点:南一
测试结果:
4)测试我要游园:
测试结果:
5)测试我想走路:
测试结果: