数据结构课设--交通咨询系统设计
一、问题描述:
交通咨询系统设计
设计目的:
熟练掌握迪杰斯特拉算法和费洛伊德算法,能够利用它们解决最短路径问题。
掌握图的深度,广度遍历算法。
掌握快速排序算法。
内容:
设计一个交通咨询系统,通过读取全国城市距离图(http://pan.baidu.com/s/1jIauHSE,请在程序运行时动态加载到内存,可将 excel 转成 csv 方便读取),
实现:
- 1、请验证全国其他省会城市(不包括港澳和两个宝岛台北和海口)到武汉中间不超过 2 个省(省会城市)是否成立?(正是因为武汉处于全国的中心位置,此次疫情才传播的如此广);
- 2、允许用户查询从任一个城市到另一个城市之间的最短路径(两种算法均要实现,界面上可自行选择)
以及所有不重复的可行路径(可限制最多经过 10 个节点),
,每一条结果均需包含路径信息及总长度,试比较排序后的结果与迪杰斯特拉算法和费洛伊德算法输出的结果; - 3、假设在求解 2 个城市间最短路径时需要绕过某个特定的城市(用户输入或者选择,例如
武汉),请问应该如何实现? - 4、不基于功能 2 遍历的结果如何直接求解两个城市间的前第 K 短的路径,例如,武汉到北
京之间第 3 短的路径。
二、需求分析:
首先需要读写文件,注意给定的格式。
在验证到其他城市武汉中间不超过 2 个省时,新建一个二维数组将原二维数组中的权值全部改为1,这样用dijkstra算法得到的权值最小的路径就经过节点最少的路径。
需要绕过某个城市时,只需要新建一个二维数组将原二维数组中的与该城市有关的边的信息全部删除(权值全部改为0)即可。
求解两个城市前K短路径不能直接调用前述的结果,需要设计新的算法直接求解。
三、概要设计:
1、数据结构定义:
程序中的城市信息主要用图来存储,图类的定义如下:
“无向图”CC_Group类的构建:
数据成员有:
int C_Adj[MaxSize][MaxSize];
int C_Visited[MaxSize];
int C_PointNum;//顶点数
int C_ArcNum;//边数
C_Adj数组用来存储边的信息,C_Adj[i][j]不为零表示存在边(i,j)且这条边上的权值为C_Adj[i][j]的值。
C_Visited[i]用来存储节点是否被访问标记,为0则未标记,为1则已经被标记。默认全部为零。
C_Point表示当前图中的节点数
C_ArcNum表示当前图中的边数
对图的初始化是用文件来完成的:
//读文件 初始化图
void CC_Graph::CreateByFile(int* PointNum, int* arcNum, int* C_BLoc, int* C_ELoc) {
CC_File2();//用文件构造图
*PointNum = 34;
for (int i = 0; i < *PointNum; i++) {
for (int j = 0; j < *PointNum; j++)
if(FilePath[i+1][j+1]==0){//无连接
C_Adj[i][j] = 0;
}
else{
C_Adj[i][j] = FilePath[i + 1][j + 1];
}
}
}
第二个功能中的所有路径是用链表作为存储结构的,每个链表存储一条路径:
typedef struct LinkList
{
int Data = -999;
LinkList* next = NULL;
}LinkList;
LinkList* CCListHead[MaxNum] ;
LinkList* Current = new LinkList;
int PathNum=0;//路径数目
int A[MaxNum];//用于排序
2、模块设计:
主程序模块:主函数设计如下:
首先创建一个图类的对象并用文件初始化它。
int PointNum = 0, arcNum = 0, C_BLoc = 0, C_ELoc = 0;
// CC_Graph G(&PointNum, &arcNum, &C_BLoc, &C_ELoc);//输入参数构造图
CC_Graph G;
G.CreateByFile(&PointNum, &arcNum, &C_BLoc, &C_ELoc);
随后是一个循环,退出条件是进入0号功能,其他功能都有对应的function,涉及到的函数调用均在对应的function中。
switch (choice)
{
case 0: {
system("cls");
cout << "\n\n\nThank You \n\n\n";
return 0;
}
case 1: {
function1(G, &PointNum, &arcNum, &C_BLoc, &C_ELoc);
break; }
case 2: {
function2( G,&PointNum, &arcNum, &C_BLoc, &C_ELoc);
break; }
case 3: {
function3(G, &PointNum, &arcNum, &C_BLoc, &C_ELoc);
break; }
case 4: {
function4(G, &PointNum, &arcNum, &C_BLoc, &C_ELoc);
break; }
}//switch结束
Dijkstra:设置Visited[]保存是否被标记,Dis[]保存用户起点到下标号的节点路径的长度,Father[]保存最短路径上的前一个节点。
算法主体有三步:
-
1更新:找到这样的两个顶点 即 i位置已经被访问 且j位置未被访问 且ij有连接,把(0,i)+(i,j)和(0,j)中的较小者赋给j,并且Father[j] = i即i作为j的前驱节点。
-
2访问:每轮在更新Dis后 判断最小未被标记的顶点 对其访问(vis = 1;)
-
重复1、2 步骤直到Visited[]的值全为1为止。
3、各模块间的调用关系:
四、详细设计
主要算法设计:
求最短路径Dijkstra算法的实现:
设置Visited[]保存是否被标记,Dis[]保存用户起点到下标号的节点路径的长度,Father[]保存最短路径上的前一个节点。其他类似于Prim算法。
算法主体有三步:
- 1.更新:找到这样的两个顶点 即 i位置已经被访问 且j位置未被访问 且ij有连接,把(0,i)+(i,j)和(0,j)中的较小者赋给j,并且Father[j] = i即i作为j的前驱节点。
- 2.访问:每轮在更新Dis后 判断最小未被标记的顶点 对其访问(vis = 1;)
- 3.重复1、2 步骤直到Visited[]的值全为1为止。
然后是输出语句:
- 输出用户输入位置到其他各个顶点的最短路径长度:
直接一个循环输出Dis数组的前n位即可 - 输出用户输入位置到其他各个顶点的最短路径对应的路线:
首先设置i从0到n的循环每次都将i赋值给VVV作为终点,v是用户输入的起点位置。
while (Father[vvv] != -1 && Father[vvv] != v)
{
cout << Father[vvv] << "<--";
vvv = Father[vvv];
}
根据Father数组内存的节点的前驱关系输出路线。
以上是迪杰斯特拉算法的基础部分,
在本程序中由于多次调用了此算法,为了减少重复的代码提高代码的复用性,于是在此算法中增加了一个接口,即一个switch case开关语句,调用迪杰斯特拉时需要在参数列表中给出需要进入的部分以实现不同的功能。
switch (choice)//不同的函数调用本函数时 进入不同的接口
{
case 0: {break; }
case 2: {
cout << "\n" << City[BLoc] << "到" << City[ELoc] << "的最短路径长度为:" << Dis[ELoc];
cout << " 轨迹为:";
int E = ELoc;
cout << City[E] << "<--";
while (Father[E] != -1 && Father[E] != BLoc)
{
cout << City[Father[E]] << "<--";
E = Father[E];
}
cout << City[BLoc];
break;
}
case 3: {break; }
case 4: {//用于A*算法
for (int i = 0; i < n; i++)
dis[i] = Dis[i];
break; }
}
CC_Floyd:
- 1,从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
- 2,对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比已知的路径更短。如果是更新它。
把图用邻接矩阵G表示出来,如果从Vi到Vj有路可达,则G[i][j]=d,d表示该路的长度;否则G[i][j]=无穷大。
定义一个矩阵D用来记录所插入点的信息,D[i][j]表示从Vi到Vj需要经过的点,初始化D[i][j]=j。把各个顶点插入图中,比较插点后的距离与原来的距离,G[i][j] = min( G[i][j], G[i][k]+G[k][j] ),如果G[i][j]的值变小,则D[i][j]=k。
在G中包含有两点之间最短道路的信息,而在D中则包含了最短通路径的信息。
比如,要寻找从V5到V1的路径。根据D,假如D(5,1)=3则说明从V5到V1经过V3,路径为{V5,V3,V1},如果D(5,3)=3,说明V5与V3直接相连,如果D(3,1)=1,说明V3与V1直接相连。
Cway[i][j] = k;//表示i到j的最短路径为i->k->j
if (Cpath[i][k] + Cpath[k][j] < Cpath[i][j])//如果插入中间节点后的路径长度比原来短
{
Cpath[i][j] = Cpath[i][k] + Cpath[k][j];//修改路径为两半路径之和
Cway[i][j] = k;//表示i到j的最短路径为i->k->j
}
与迪杰斯特拉算法类似本程序中也多次调用了Floyd算法,所以也类似的设计一个接口:
case 2: {//输出AB两座城市
cout << "\n" << City[BLoc] << "到" << City[ELoc] << "的最短路径长度为:" << Cpath[BLoc][ELoc];
cout << " 轨迹为:";
int u = BLoc;//起点
while (Cway[u][ELoc] != -1) {//有中间节点就把中间节点 赋给起点 不断循环
cout << City[u] << "->";
u = Cway[u][ELoc];
}
cout << City[ELoc]<<endl;
break;
}
case 3: {break; }
case 4: {break; }
}
Allpath:
假设我们要找出结点3到结点6的所有路径,那么,我们就设结点3为起点,结点6为终点。我们需要的存储结构有:一个保存路径的栈、一个保存已标记结点的数组,那么找到结点3到结点6的所有路径步骤如下:
1、 我们建立一个存储结点的栈结构,将起点3入栈,将结点3标记为入栈状态;
2、 从结点3出发,找到结点3的第一个非入栈状态的邻结点1,将结点1标记为入栈状态;
3、 从结点1出发,找到结点1的第一个非入栈状态的邻结点0,将结点0标记为入栈状态;
4、 从结点0出发,找到结点0的第一个非入栈状态的邻结点2,将结点2标记为入栈状态;
5、 从结点2出发,找到结点2的第一个非入栈状态的邻结点5,将结点5标记为入栈状态;
6、 从结点5出发,找到结点5的第一个非入栈状态的邻结点6,将结点6标记为入栈状态;
7、 栈顶结点6是终点,那么,我们就找到了一条起点到终点的路径,输出这条路径;
8、 从栈顶弹出结点6,将6标记为非入栈状态;
9、 现在栈顶结点为5,结点5没有除终点外的非入栈状态的结点,所以从栈顶将结点5弹出;
10、现在栈顶结点为2,结点2除了刚出栈的结点5之外,还有非入栈状态的结点6,那么我们将结点6入栈;
11、现在栈顶为结点6,即找到了第二条路径,输出整个栈,即为第二条路径
12、重复步骤2-11,就可以找到从起点3到终点6的所有路径;
13、栈为空,算法结束。
路径的存储:
CCListHead[i]->data 是第i条路径的长度 CCListHead[i]->next->data等是路径上的节点 ;
排序:CCListHead[i](0~n)用快速排序进行排序
快排算法:
快速排序(改进版)(交换排序的一种)
递归快排:
改进方案:改进选取枢轴的方法,即每次选取数据集中的中位数做枢轴,(选取中位数的可以在 O(n)时间内完成)。
快排的分割策略:
第一步是通过将枢轴元与最后一个元素交换使得枢轴元离开要被分割的数据段;i 从第一个元素开始而 j 从倒数第二个元素开始。当 i 在 j 左边时,我们将 i 右移,移过那些小于枢轴元的元素,并将 j 左移,移过那些大于枢轴元的元素。当 i 和 j 停止时,i 指向的是大元素,j指向的是小元素。如果 i 在 j 左边,那么将这两个元素互换。 如果此时 i 和 j 已经交错即 i>j所以不交换。此时把枢轴元与 i 所指的元素交换。
//返回中位数位置
int GetMiddleValue(int A[], int low, int high)
{
// int mid = low + (high - low) >> 1;
int mid = (high + low) / 2;
int y1 = A[low] > A[mid] ? low : mid;
int y2 = A[low] > A[high] ? low : high;
int y3 = A[mid] > A[high] ? mid : high;
if (y1 == y2) return y3;
else return A[y1] > A[y2] ? y2 : y1;
}
CC_File2:
先将文件“连接”读入到FilePath[][],0表示无连接,1表示有连接
再把文件“距离”读入到FilePath[][],若ij有连接则把FilePath[i][]改为文件中读到的权值,若ij无连接则保持FilePath[i][]为0即可。
ifstream in_file("省会城市邻接表.txt", ios::in);
hang = 2;//原文件第一行为空白
while (in_file)
{
string s;
getline(in_file, s);
int lie = 1;
for (int i = 0; i < s.length(); i++)
{
if (s[i] != ',')
{
string c = "";
while (s[i] != ',')
{
if (i >= s.length()) break;
c += s[i];
i++;
// if (i >= s.length()) break;
}
if (FilePath[hang][lie] == 1 )//1表示有连接
{
FilePath[hang][lie] = stoi(c.c_str());
FilePath[lie][hang] = stoi(c.c_str());
//InsertEdge(hang, lie, stoi(c.c_str()));
// lie++;
}
lie++;
}
}
hang++;
}
in_file.close();
for (int i = 0; i <= 34; i++)
for (int j = 0; j <= 34; j++)
if (i == j)
FilePath[i][j] = 0;
for (int i = 1; i <= 34; i++) {
cout << endl;
for (int j = 1; j <= 34; j++)
cout << FilePath[i][j] << "\t";
}
cout << "\n读取文件处理完毕\n";
A_Star:
在A*算法中用优先队列就是要用到启发函数f(s)确定状态在优先队列里面的优先级。解决这道题的时候选取h(x)=dt(x), dt(x)是x节点到目标节点的最短距离,其开始由Dijkstra直接求得。
控制每个节点的入队(或出队)次数为k次,可以找到第k短路径。
struct a_star //A*搜索时的优先级队列
{
int v;//当前指示的节点
int len;//到起点的距离
bool operator<(const a_star& a)const //f(i)=d[i]+h[i] h(i)表示i到end的最短路(储存在dis[]中,由djikstra给出)
{
return len + dis[v] > a.len + dis[a.v];//len + dis[v]值小的 在优先级队列里面有更高的优先级
}
};
int C_Astar(int Path[MaxSize][MaxSize],int N,int Begin ,int End ,int K,int ans[MaxSize])
{
if (Begin == End)
K++;
if (dis[Begin] == MaxNum) {
return -1;
}
a_star n1;//a_star
n1.v = Begin;
n1.len = 0;
priority_queue <a_star> q;//优先级队列
q.push(n1);//初始状态 起点入队
while (!q.empty())
{
a_star temp = q.top();//temp是当前有最大优先级的元素
q.pop();
ans[temp.v]++;//计数器,表示v节点访问次数加一
if (ans[End] == K)//当第K次取终点的时候,输出路程
return temp.len;
if (ans[temp.v] > K)//v已经入队k次 则直接 跳过
continue;
int i = temp.v;
for (int j = 0; j < N; j++)
if (Path[i][j] != 0) {//遍历所有与i有连接的点j
a_star n2;
n2.v = j;
n2.len = Path[i][j] + temp.len;
q.push(n2);
}
}
return -1;
}
所有路径结果写入文件:
//输出文件格式 :序号,长度,A->B->C->end;
ofstream out;
out.open("结果.txt");
for (int i = 0; i < PathNum; i++)
{
LinkList* p = CCListHead[i];
out << i + 1 << ":--长" << p->Data << " ";
p = p->next;
while (p->next!= NULL)
{
out << City[p->Data] << "->";
p = p->next;
}
out << City[p->Data] << endl;
}
out.close();
五、用户手册:
打开程序后会显示菜单和城市信息,选择需要的功能输入即可,注意本程序输入的城市是城市对应的编号(菜单给出对应关系)而不是城市名称。
- 功能一:选择此功能可以验证全国任一城市到其他所有城市之间的城市数,如果要验证题目中所述即全国其他省会城市(不包括港澳和两个宝岛台北和海口)到武汉中间不超过 2 个省,只需输入编号22即可。
- 功能二:选择此功能后需要再输入待求的两个城市的编号,程序会自动输出三种算法算出的最短路径长度和路线,并输出两座城市间所有不重复的可行路径(最多经过 10 个节点)以及所有路径的数量,排序后程序输出前十五条短的路径,并将所有路径信息写入文件。
六、测试结果:
表 1 功能一
表 2 功能二
表 3 功能三
表 4 功能四