题目:
使用C语言/C++设计校园跑最短路径。
参考代码:
cpp
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<limits.h>
#define MAX_VERTICES 10
#define INF INT_MAX
/*INT_MAX:C语言标准库中定义的常量,表示int类型的最大值,通常为2,147,483,647,
此时表示顶点之间无法直达*/
/*#define INF (INT_MAX / 2)更加安全,因为可以防止累加时溢出;
最保守就是#define INF 1000000,使用一个足够大的数代替INT_MAX,避免溢出*/
//图的顶点的结构体
typedef struct
{
//1.顶点名称(地点名)
char* name;
//2.顶点编号
int id;
} Vertex;
//图的结构体定义
typedef struct
{
//1.顶点数量
int vertexCount;
//2.顶点数组->存储顶点
Vertex vertices[MAX_VERTICES];
//3.邻接矩阵存储边权重(本实验采取邻接矩阵存储图->邻接矩阵表示图的具体内容)
int adjacencyMatrix[MAX_VERTICES][MAX_VERTICES];
//4.Floyd算法距离矩阵
/*比如dist[3][5] = 150意味着从3号点位到5号点位目前找到的最短路径是150米*/
int dist[MAX_VERTICES][MAX_VERTICES];
//5.Floyd算法路径矩阵
/*假设path[1][7] = 4,说明从1号点去7号点时,建议先到4号点中转*/
int path[MAX_VERTICES][MAX_VERTICES];
}Graph;
/*------------------------------------------赵旭文------------------------------------------*/
//初始化图的函数
void initGraph(Graph* graph) {
//1.初始时图中没有任何顶点,因此顶点个数为0
graph->vertexCount = 0;
//2.本例采用邻接矩阵存储图
/*对角线初始化为0,其余为无穷大,因为对角线表示自己到自己*/
for (int i = 0; i < MAX_VERTICES; i++) { //外层循环是行
for (int j = 0; j < MAX_VERTICES; j++) { //内层循环是列
//2.1.初始化边的权值,自己到自己为0,其余为无穷即不存在(i==j时结果为true,就为0)
graph->adjacencyMatrix[i][j] = (i == j) ? 0 : INF;
//2.2.初始化距离,初始时对角顶点的距离为0
graph->dist[i][j] = (i == j) ? 0 : INF;
//2.3.初始中转点,初始时路径矩阵初始化为-1,代表没有任何中转点
graph->path[i][j] = -1;
}
}
}
//图中插入顶点的函数
/*形参需要目标图,插入顶点的名称以及插入顶点的编号*/
int insertVertex(Graph* graph,const char* name, int id) {
//1.判断图是否已满,若满了就无法继续添加
if (graph->vertexCount >= MAX_VERTICES)
{
printf("图已满,无法插入新顶点。\n");
return -1;
}
//2.此时可以插入新顶点,需要判断顶点编号是否越界
if (id < 0 || id >= MAX_VERTICES)
{
printf("顶点ID超出范围。\n");
return -1;
}
//3.继续检查顶点id是否已存在
for (int i = 0; i < graph->vertexCount; i++)
{
if (graph->vertices[i].id == id)
{
printf("顶点ID %d 已存在。\n", id);
return -1;
}
}
//4.此时要插入的顶点合法
/*graph->vertices[graph->vertexCount].name = (char*)name;
->如果需要可以强制转换为变量,const为常量 */
/*分配内存并复制字符串,
对于malloc函数:内存分配失败时返回空指针NULL,成功时返回分配内存的起始地址(void*类型指针),
因此为了保证安全性,调用malloc函数后要强制转换成所需类型*/
graph->vertices[graph->vertexCount].name = (char*)malloc(strlen(name) + 1);
if (graph->vertices[graph->vertexCount].name == NULL)
{
printf("内存分配失败。\n");
return -1;
}
strcpy(graph->vertices[graph->vertexCount].name, name);
graph->vertices[graph->vertexCount].id = id;
graph->vertexCount++;
//5.正常返回
return 0;
}
//添加图中边的权值的函数(即设置边之间的距离)
/*形参需要图,需要操作的两个顶点的编号、要设置的顶点间的距离*/
void addEdge(Graph* graph, int id1, int id2, int distance) {
//1.定义两个变量要来查找要操作的两个顶点在图中是否存在
int index1 = -1, index2 = -1;
//2.查找要操作的顶点在图中对应的索引
for (int i = 0; i < graph->vertexCount; i++)
{
if (graph->vertices[i].id == id1) index1 = i;
if (graph->vertices[i].id == id2) index2 = i;
}
//3.判断是否查找成功->只要有一个顶点不在图中,那么所操作的边就不存在
if (index1 == -1 || index2 == -1)
{
printf("顶点ID不存在。\n");
return;
}
//4.此时可以设置边的权值
/*由于是无向图,所以两个顶点间来回的距离一样*/
graph->adjacencyMatrix[index1][index2] = distance;
graph->adjacencyMatrix[index2][index1] = distance;
graph->dist[index1][index2] = distance; //两顶点间当前最短路径就是此时设置的距离
graph->dist[index2][index1] = distance;
}
//插入校园跑中各个顶点以及设置顶点之间的权值的函数
void setVertex_Edge(Graph* graph)
{
//1.插入5个校园地点
printf("初始化校园跑地点... \n");
insertVertex(graph, "图书馆", 0); //赋值过去的地点是常量
insertVertex(graph, "教学楼", 1);
insertVertex(graph, "体育馆", 2);
insertVertex(graph, "食堂", 3);
insertVertex(graph, "宿舍", 4);
//2.设置各个地点之间的距离(单位:米)
printf("设置地点间距离... \n");
addEdge(graph, 0, 1, 300); // 图书馆-教学楼
addEdge(graph, 0, 2, 500); // 图书馆-体育馆
addEdge(graph, 0, 3, 400); // 图书馆-食堂
addEdge(graph, 0, 4, 600); // 图书馆-宿舍
addEdge(graph, 1, 2, 350); // 教学楼-体育馆
addEdge(graph, 1, 3, 450); // 教学楼-食堂
addEdge(graph, 1, 4, 550); // 教学楼-宿舍
addEdge(graph, 2, 3, 200); // 体育馆-食堂
addEdge(graph, 2, 4, 700); // 体育馆-宿舍
addEdge(graph, 3, 4, 300); // 食堂-宿舍
}
/*------------------------------------------赵旭文------------------------------------------*/
/*------------------------------------------青格勒------------------------------------------*/
//使用Floyd算法计算所有顶点对之间的最短路径的函数
/*Floyd算法计算了所有地点之间的最短路径*/
void floydAlgorithm(Graph* graph) {
//1.初始化距离矩阵和路径矩阵
for (int i = 0; i < graph->vertexCount; i++) //外层循环为行
{
for (int j = 0; j < graph->vertexCount; j++) //内层循环为列
{
/*1.1.初始时各个顶点间的最短路径就是各个顶点间的距离*/
graph->dist[i][j] = graph->adjacencyMatrix[i][j];
/*1.2.特殊判断*/
if (graph->dist[i][j] < INF && i != j)
{
//表示如果i和j直接相连,路径经过i
graph->path[i][j] = i;
}
else
{
//此时表示不直接相连或者i为j
graph->path[i][j] = -1;
}
}
}
//2.使用Floyd算法得出最短路径
for (int k = 0; k < graph->vertexCount; k++) /*外层循环k表示当前中转点*/
{
for (int i = 0; i < graph->vertexCount; i++) /*内层两个循环用来判断新的中转点和最短路径长度*/
{
for (int j = 0; j < graph->vertexCount; j++)
{
/*如果通过k顶点可以使路径更短*/
if (graph->dist[i][k] < INF && graph->dist[k][j] < INF &&
graph->dist[i][j] > graph->dist[i][k] + graph->dist[k][j])
{
//更新i和j之间的最短路径和中转顶点为k(从i到j改为更近的k到j)
graph->dist[i][j] = graph->dist[i][k] + graph->dist[k][j];
graph->path[i][j] = graph->path[k][j];
}
}
}
}
}
//获取从地点start到地点end的最短路径详情的函数
/*形参需要图、起点、终点、中转地点数组、
记录一条完整路径中包含的顶点总数(C语言函数参数默认是值传递,要修改外部变量必须用指针)*/
void getShortestPath(Graph* graph, int start, int end, int path[], int* pathLength) {
//1.判断起点到终点是否是不可达到
if (graph->dist[start][end] == INF)
{
//此时代表起点到终点无法达到,直接结束函数
*pathLength = 0;
return;
}
//2.此时起点可以到达终点
/*2.1.定义中间中转地点数组记录中转地点*/
int tempPath[MAX_VERTICES];
/*2.2.定义变量记录当前处理的地点*/
int index = 0;
/*2.3.定义变量记录当前终点,最开始就是形参里的终点*/
int current = end;
//3.反向追踪路径
/*之所以反向追踪,是因为如果正向追踪,只是知道了目的地,但不知道从起点出发下一步要去哪儿,
反向追踪的话只需要查看前一站是哪里即可,
类似跑步时回头看脚印,直接沿着成功的路径返回,避免重复探索*/
while (current != start) //只要没到起点就一直往前找
{
//4.把当前终点赋值给index上的地点,然后index++
tempPath[index++] = current;
//5.得出当前终点current的前一个顶点
/*graph->path[start][current]存储的是:从start到current的最短路径中,
current的前一个顶点是谁,也就是start到current的中转地点*/
current = graph->path[start][current];
/*6.安全判断:如果current变成-1,说明路径追踪过程中出现了断裂,
-1在path矩阵中表示"没有前驱顶点"或"不可达",
这种情况应该立即终止函数,避免无限循环 */
if (current == -1)
{
*pathLength = 0;
return;
}
}
/*7.循环结束后:补上起点。
为什么需要这一步?while循环在current == start时终止,所以起点没有被记录,
需要手动将起点加入路径数组,此时tempPath中存储的是完整的反向路径*/
tempPath[index++] = start; //这里是先赋值到index上即while结束时指向的start,再index++
//8.反转路径并再反转,就是完整的最短路径内容
/*8.1.记录一条完整路径中包含的顶点总数:index经过while循环和起点补充后,
index的值等于路径中顶点的总数,
index为0,多一个顶点就++,最后多了个起点也++*/
*pathLength = index;
/*8.2.反转中转顶点存入path,就是完整的最短路径,
index - 1:获取最后一个有效索引,因为数组索引从0开始,所以最大索引是元素个数减1;
- i:从后往前遍历,比如当i=0时,取最后一个元素*/
for (int i = 0; i < index; i++)
{
path[i] = tempPath[index - 1 - i];
}
}
//打印任意两地点间的最短路径的函数
void printAllShortestPaths(Graph* graph) {
printf("============================================ \n");
printf("各个地点间的最短路径信息:\n");
//1.循环打印各个顶点到各个顶点之间的最短距离
for (int i = 0; i < graph->vertexCount; i++) //外层循环为行
{
for (int j = i + 1; j < graph->vertexCount; j++) //内层循环为列
{
//2.进行判断两个地点之间是否是无穷的距离
if (graph->dist[i][j] < INF)
{
/*3.此时两个地点之间可达,输出两个地点间的最短路径长度,
最短路径长度之前已经通过Floyd算法得出 */
printf("%s →%s:%d米\n",
graph->vertices[i].name,
graph->vertices[j].name,
graph->dist[i][j]);
//4.打印详细最短路径
/*4.1.定义数组path记录中转地点*/
int path[MAX_VERTICES];
/*4.2.定义变量记录一条完整路径中包含的顶点总数,
初始为0,因为一开始没给出起点和终点*/
int pathLength;
/*4.3.调用getShortestPath函数得出最短路径详情*/
getShortestPath(graph, i, j, path, &pathLength);
//4.4.打印起点到终点的最短路径详情
printf(" 路径:");
for (int k = 0; k < pathLength; k++)
{
//首先打印当前顶点
printf("%s", graph->vertices[path[k]].name);
//再打印一个指向下一个顶点的符号,注:(最后一个地点后不加箭头)
if (k < pathLength - 1)
{
printf(" → ");
}
}
printf("\n");
}
else
{
//此时地点之间的距离为无穷,不可达
printf("%s →%s:不可达\n",
graph->vertices[i].name,
graph->vertices[j].name);
}
printf("--------------------------------------------\n");
}
}
}
/*------------------------------------------青格勒------------------------------------------*/
/*------------------------------------------黄黄------------------------------------------*/
//用来寻找访问所有地点的最短环路的函数
void findShortestTour(Graph* graph) {
//1.判断能否执行,至少需要两个地点
if (graph->vertexCount < 2)
{
printf("地点数量不足,无法计算环路。\n");
return;
}
/*此时可以计算环路*/
//2.使用最近邻算法作为近似解,环路方案使用最近邻算法,是近似最优解
/*最近邻算法:贪心策略,每次选择最近的未访问点;
近似解:承认不是绝对最优,但在可接受时间内找到较好解;
现实考量:完全遍历所有排列在顶点多时计算量爆炸*/
/*2.1.定义访问标记数组:初始全为0,表示所有地点都未访问,
访问后标记为1,避免重复访问*/
int visited[MAX_VERTICES] = {0}; //之后的就是int的默认值即0
/*2.2.定义环路路径记录器,大小为顶点数+1,因为最后要回到起点*/
int tour[MAX_VERTICES + 1];
/*2.3.定义总里程累加器,从0开始,每走一段路就累加距离*/
int totalDistance = 0;
//3.环路的起跑线设置
/*3.1.选择图书馆(顶点0)作为起点(也可选择其他顶点)*/
int current = 0;
/*3.2.记录起点:环路数组的第一个位置存储起点*/
tour[0] = current;
/*3.3.标记起点已访问*/
visited[current] = 1;
//4.贪心构建路径
for (int step = 1; step < graph->vertexCount; step++)
{
/*外层循环:逐个添加剩余地点,step从1开始,因为起点已经在第0位置*/
//5.寻找"最近邻居"
/*5.1.定义下一个目标地点next,-1表示尚未找到*/
int next = -1;
/*5.2.minDist表示当前最小距离,初始设为无穷大*/
int minDist = INF;
//6.寻找最近的未访问顶点
for (int i = 0; i < graph->vertexCount; i++)
{
/*7.!visited[i]:检查地点i是否未被访问,
graph->dist[current][i] < minDist:检查是否比当前找到的最近点更近,
如果两个条件都满足:更新最小距离和目标地点*/
if (!visited[i] && graph->dist[current][i] < minDist)
{
minDist = graph->dist[current][i];
next = i;
}
}
/*8.安全防护:如果找不到下一个地点(理论上不应发生),立即终止循环,
防止无限循环或数组越界*/
if (next == -1) break;
//9.更新状态,准备下一轮
/*9.1.将选中的地点加入环路路径*/
tour[step] = next;
/*9.2.累加这段路的距离*/
totalDistance += minDist;
/*9.3.标记该地点已访问*/
visited[next] = 1;
/*9.4.移动当前位置指针到新地点*/
current = next;
}
//10.完成环路,回到起点,环路的"收尾工作"
/*10.1.加上返回起点的距离,当前在最后一个访问地点,需要回到起点完成环路*/
/*起点到各个地点的距离在算法前期已经通过 graph->dist 矩阵完整计算了,
Floyd算法已经计算了所有顶点间的最短距离,
*/
totalDistance += graph->dist[current][tour[0]];
/*10.2.路径数组的闭环,最后一个位置存储起点,形成完整的环形路径*/
tour[graph->vertexCount] = tour[0];
//11.输出打印整个环路
printf("校园跑最佳环路方案(近似解):\n");
printf("最佳路线:");
for (int i = 0; i <= graph->vertexCount; i++) //包含了i为graph->vertexCount,因为环路会回到起点
{
printf("%s", graph->vertices[tour[i]].name);
if (i < graph->vertexCount) //最后一个地点不打印箭头
{
printf(" → ");
}
}
printf("\n 总距离:%d米\n", totalDistance);
//12.输出详细路径信息
printf("\n路径详情:\n");
for (int i = 0; i < graph->vertexCount; i++)
{
int from = tour[i];
int to = tour[i + 1];
printf("从 %s 到 %s:%d米\n",
graph->vertices[from].name,
graph->vertices[to].name,
graph->dist[from][to]);
}
printf("============================================ \n");
}
/*------------------------------------------黄黄------------------------------------------*/
/*------------------------------------------吴佳峻------------------------------------------*/
//打印各个顶点之间的距离矩阵的函数
void printDistanceMatrix(Graph* graph) {
printf("============================================\n");
printf("当前各个地点之间的距离如下:\n");
printf(" ");
//1.首先遍历输出所有顶点即地点
for (int i = 0; i < graph->vertexCount; i++)
{
printf("%-8s", graph->vertices[i].name);
}
printf("\n");
//2.再输出打印各个顶点到各个顶点的距离
for (int i = 0; i < graph->vertexCount; i++)
{
/*2.1.首先打印当前地点名称*/
printf("%-5s", graph->vertices[i].name);
/*2.2.遍历所有地点,打印当前地点距离各个地点有多远*/
for (int j = 0; j < graph->vertexCount; j++)
{
if (graph->dist[i][j] == INF)
{
printf("INF ");
}
else
{
printf("%-8d", graph->dist[i][j]);
}
}
printf("\n");
}
printf("============================================\n");
}
/*------------------------------------------吴佳峻------------------------------------------*/
int main() {
//1.创建图(校园跑)
Graph campusGraph;
//2.初始化图
initGraph(&campusGraph);
//3.设置校园跑顶点和距离
setVertex_Edge(&campusGraph);
//4.打印未求最短距离的矩阵
printDistanceMatrix(&campusGraph);
//5.使用Floyd算法计算最短路径
printf("执行Floyd算法计算最短路径...\n");
floydAlgorithm(&campusGraph);
//6.打印求出最短距离的矩阵
printDistanceMatrix(&campusGraph);
//7.打印所有最短路径
printAllShortestPaths(&campusGraph);
//8.寻找最佳环路
findShortestTour(&campusGraph);
return 0;
}