题目要求:
针对我们的校园布局,设计一款校园平面图。
基本功能及要求如下:
1、 提供用户操作的菜单和界面,实现系统的所有功能。
2、 添加校内地点的信息,例如校门、教学楼、食堂、宿舍、快递点等,每个地点视为一个顶点,具体信息包括:名称、坐标、简介等。
3、 添加道路信息,可理解为顶点之间的边,也可以理解为顶点在路边。
4、 以上添加的地点和道路信息可以修改和删除,即具备信息的增删改功能。并且所有信息要求以文件的形式存储(如文本文件),格式自行设计。当下次运行程序时,从文件读取地点和道路信息。
5、 打印校园地图。
6、 设计出一套铺设线路最短,但能满足每个地点都能通电的方案。两点之间的距离根据坐标计算。输出铺设电路规划以及每条电路的长度和总长度。
7、 查询校园地点信息,查询该地点到其他任一地点的最短简单路径及距离,并按照距离从近到远的顺序显示。
8、 求从某一地点出发经过校园所有地点(地点可以重复)且距离最短的路线;求从某一地点出发经过校园所有地点(地点可以重复)并回到源点的最短路线。
程序具体实现
1、数据结构和初始设置
struct Vertex{
string name;//地点名称
string info;//地点简介
int x,y;//地点坐标
};
struct Graph{
double adjmatrix[numbervertex][numbervertex];//邻接矩阵--边表,可视为边之间的关系,存放权值
Vertex v[numbervertex]; //创建节点数组存储简介、名称、坐标
int n,e;//当前的结点个数
};
如图,数据结构为邻接矩阵,其中Graph的元素有节点个数、节点数组、存放边权重的邻接矩阵,其中Vertex的元素有地点名称、地点简介和地点坐标。
#define numbervertex 100//最大点数为100
#define INF 99999//用于邻接矩阵中,表示无路
#include<iostream>
#include<string>
#include <cmath>
#include <vector>
#include <queue>
#include<limits>
#include <cstdlib> //为使用system
#include<bits/stdc++.h>//用sqrt()计算坐标
#include <iomanip>
using namespace std;
值得一提的是图中numbervertex是邻接矩阵的最大容量,INF用于描述两节点间没有边的情况。
2、欢迎界面和菜单界面
void welcome() {
// 欢迎界面
cout << "————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————\n\n";
cout << " 【武汉科技大学地图】\n\n";
cout << " 欢迎使用武汉科技大学地图\n";
cout << "————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————\n\n";
cout << "按任意键继续...";
cin.get(); //暂停
system("cls");
}
void menu() {
// 主菜单
cout << " 请选择要进行的操作: (0:退出)" <<endl;
cout << " 1:增加校园地点 2:增加校园地点间的路径" <<endl;
cout << " 3:删除校园地点 4:删除校园地点间的路径" <<endl;
cout << " 5:打印校园地图 6:设计校园电路最短路径" <<endl;
cout << " 7:查询校园信息 8:从某一地点出发走遍校园的最短路线" <<endl;
cout << " 9:求从某一地点出发走遍校园并回到源点的最短路线" <<endl;
}
welcome()和menu()函数的实现非常简单,直接暴力输出即可,唯二需要注意的是调用<iostream>库函数cin.get(),输入一个字符(任意按一个键)达到一个页面暂停的效果 ;通过<cstdlib>库中system函数,发出("cls")指令,该命令用于清除窗口的内容。(注:只可用于Windows系统)
3、增删地点与路径
void add_node(Graph& m) {
// 图中多了一个节点
if (m.n < numbervertex) { // 确保节点数量没有超出最大容量
m.n++; // 增加节点数量
cout << "请输入地点名称:" << endl;
cin >> m.v[m.n - 1].name; // 输入地点名称
cout << "请输入地点简介:" << endl;
cin >> m.v[m.n - 1].info; // 输入地点简介
cin.ignore(numeric_limits<streamsize>::max(), '\n'); // 忽略缓存区中的换行符
cout << "请输入节点坐标 (x y):" << endl;
cin >> m.v[m.n - 1].x >> m.v[m.n - 1].y; // 输入节点坐标
if( m.v[m.n - 1].x>100|| m.v[m.n - 1].y>100|| m.v[m.n - 1].y<0||m.v[m.n - 1].x<0){
cout<<"请输入正确坐标!"<<endl;
return ;
}
// 初始化新节点的邻接边
for (int i = 0; i < numbervertex; ++i) {
m.adjmatrix[m.n - 1][i] = INF;
m.adjmatrix[i][m.n - 1] = INF;
}
m.adjmatrix[m.n - 1][m.n - 1] = 0; // 该节点到自己的距离为0
cout << "添加成功!" << endl;
} else {
cout << "地图已满,无法添加更多地点。" << endl;
}
}
void add_edge(Graph& m, int a, int b) {
// 检查两个节点之间的边是否已经存在
if (m.adjmatrix[a][b] == 99999) {
// 如果不存在边,则添加边并更新邻接矩阵
m.adjmatrix[a][b] = distance(m.v[a],m.v[b]); // 设置节点a到节点b的边的权值
m.adjmatrix[b][a] = distance(m.v[a],m.v[b]); // 因为是无向图,所以也要设置节点b到节点a的边的权值
m.e=m.e+2; // 增加边的数量
cout << "添加成功!" << endl; // 提示用户添加成功
} else {
// 如果边已经存在,则提示用户输入错误
cout << "该两点已有边,输入错误!" << endl;
}
}
void del_node(Graph& m, int v) {
// 检查节点是否存在
if (v < 0 || v >= m.n) {
cout << "该地点不存在,输入错误!" << endl;
return;
}
// 将要删除节点的所有边设置为INF
for (int i = 0; i < m.n; ++i) {
m.adjmatrix[v][i] = INF;
m.adjmatrix[i][v] = INF;
}
// 移动后续节点的数据和邻接表条目
for (int i = v; i < m.n - 1; ++i) {
m.v[i] = m.v[i + 1]; // 移动节点信息
for (int j = 0; j < m.n; ++j) {
m.adjmatrix[i][j] = m.adjmatrix[i + 1][j]; // 移动邻接矩阵条目
m.adjmatrix[j][i] = m.adjmatrix[j][i + 1]; // 因为是无向图,所以两个方向都要更新
}
}
// 减少节点数量
m.n--;
cout << "删除成功!" << endl;
}
void del_edge(Graph& m, int a, int b){
// 删除地点间的路径
if (a < 0 || a >= m.n || b < 0 || b >= m.n){
cout << "无效的节点编号。" << endl;
return;
}
// 检查两个方向是否都有边
if (m.adjmatrix[a][b] != INF && m.adjmatrix[b][a] != INF) {
m.adjmatrix[a][b] = INF;
m.adjmatrix[b][a] = INF;
m.e -= 2; // 因为是无向图,所以删除两个方向的边
cout << "删除成功!" << endl;
} else {
cout << "该路径不存在,输入错误!" << endl;
}
}
double distance(const Vertex& a, const Vertex& b) {
return std::sqrt(std::pow(a.x - b.x, 2) + std::pow(a.y - b.y, 2));
}
设计思路十分简单
增加节点:
1、判断是否超过最大容量;(健壮性)
2、输入地点信息(名称、简介和坐标)
3、设置邻接矩阵,将其与其他节点距离设置为INF,即无路径;
删除节点:
1、判断该节点是否存在;(节点索引是否超过图的节点数)
2、将其与其他节点的距离都设置为INF,除了其所在的这一行,还有这一列;
3、移动剩余节点在图的节点数组和邻接矩阵中的位置,顺序表的删除;
增加边:
1、先判断是否存在边;
2、若不存在,更新邻接矩阵中的权重,权重由该二节点的坐标计算,末尾附上坐标计算函数;
删除边以此类推
4、输出校园地图函数
因为时间有限,进度很赶,该函数的实现比较粗糙,若两节点间有边存在,两点之间便有水平方向和竖直方向的线链接;
该函数用一个二维字符数组(函数中用向量来表示)来打印图,将数组初始化为空格符;在每个节点坐标对应点输出该地点的索引。
需要注意的是平面直角坐标系中的横纵坐标顺序与二维数组中的行列顺序是相反的。
void printCampusMap(const Graph& graph) {
const int mapSize = 100; // 地图大小硬编码为100
std::vector<std::vector<char>> map(mapSize, std::vector<char>(mapSize, ' '));
// 初始化地图
for (int i = 0; i < mapSize; ++i) {
for (int j = 0; j < mapSize; ++j) {
map[i][j] = ' ';
}
}
// 标记顶点位置
for (int i = 0; i < graph.n; ++i) {
if (graph.v[i].x >= 0 && graph.v[i].x < mapSize &&
graph.v[i].y >= 0 && graph.v[i].y < mapSize) {
map[graph.v[i].y][graph.v[i].x] = '0' + i; // 注意这里行列的索引顺序
}
}
// 绘制边
for (int i = 0; i < graph.n; ++i) {
for (int j = i + 1; j < graph.n; ++j) {
if (graph.adjmatrix[i][j] != INF) {
// 确定起始点和结束点
int startX = std::min(graph.v[i].x, graph.v[j].x);
int startY = std::min(graph.v[i].y, graph.v[j].y);
int endX = std::max(graph.v[i].x, graph.v[j].x);
int endY = std::max(graph.v[i].y, graph.v[j].y);
// 绘制水平线
for (int x = startX + 1; x < endX; ++x) {
map[endY][x] = '-';
}
// 绘制垂直线
for (int y = startY + 1; y < endY; ++y) {
map[y][endX] = '|';
}
}
}
}
// 打印地图
for (const auto& row : map) {
for (char ch : row) {
cout << ch;
}
cout << endl;
}
}
5、校园电路规划函数
该功能十分明显需要用到最小生成树算法来实现,考虑到或许会在图中添加大量的路径,该功能采用Prim算法。
采用三个向量(数组)存储访问情况、父节点和最短距离,然后根据update、scan、add三步骤重复执行直到每个节点都被访问即可。
void circuit_Prim(const Graph& graph) {
int numVertices = graph.n;
vector<bool> visited(numVertices, false); // 记录顶点是否已访问
vector<int> parent(numVertices, -1); // 记录最小生成树中每个顶点的父顶点
vector<double> key(numVertices, INF); // 记录从起始顶点到其他顶点的最短距离
key[0] = 0; // 起始顶点到自身的距离为0
int u = -1;
double totalLength = 0; // 总电路长度
for (int i = 0; i < numVertices; ++i) {
double min = INF;
// 找到未访问顶点中具有最小key值的顶点
for (int j = 0; j < numVertices; ++j) {
if (!visited[j] && key[j] < min) {
min = key[j];
u = j;
}
}
// 将顶点u加入生成树中
visited[u] = true;
totalLength += min;
// 更新其他顶点的key值
for (int v = 1; v < numVertices; ++v) {
if (!visited[v] && graph.adjmatrix[u][v] < key[v]) {
key[v] = graph.adjmatrix[u][v];
parent[v] = u;
}
}
}
// 输出电路规划和总长度
cout << "最优电路规划为:" << endl;
for (int i = 0; i < numVertices; ++i) {
if (parent[i] != -1) {
cout << "从 " << graph.v[parent[i]].name << " -> " << graph.v[i].name << " (长度: " << key[i] << ")" << endl;
}
}
cout << "电路总长度为: " << totalLength << endl;
}
6、查询校园地点信息函数
题目要求纳入简单路径,寻找与该节点直接连接的节点即可。由于该功能为单源点问题,选择Dijkstra算法。
void search(const Graph& m, int startVertex) {
int numVertices = m.n;
std::vector<std::pair<int, double>> distances(numVertices); // 存储顶点索引和距离
// 初始化距离向量为无穷大
for (int i = 0; i < numVertices ; ++i) {
distances[i] = std::make_pair(i, INF);
}
// 开始顶点到自身的距离为0
distances[startVertex] = std::make_pair(startVertex, 0);
// 使用Dijkstra算法找到最短路径
for (int i = 0; i < numVertices; ++i) {
int current = -1;
double minDistance = INF;
// 找到距离最小的顶点
for (const auto& dist : distances) {
if (dist.second < minDistance && !m.v[dist.first].name.empty()) {
minDistance = dist.second;
current = dist.first;
}
}
if (current == -1) break; // 所有顶点都已处理
// 更新相邻顶点的距离
for (int j = 0; j < numVertices; ++j) {
if (m.adjmatrix[current][j] != INF) {
double tentativeDistance = minDistance + m.adjmatrix[current][j];
if (tentativeDistance < distances[j].second) {
distances[j] = std::make_pair(j, tentativeDistance);
}
}
}
}
// 根据距离对向量进行排序
std::sort(distances.begin(), distances.end(), [](const std::pair<int, double>& a, const std::pair<int, double>& b) {
return a.second < b.second;
});
// 输出最短路径
std::cout << "从 " << m.v[startVertex].name << " 到其他地点的最短路径为:" << std::endl;
for (const auto& dist : distances) {
if (dist.second != INF && dist.first != startVertex) { // 排除到自身的距离
cout << "到地点 " << m.v[dist.first].name << "距离为: " << dist.second <<endl;
}
}
}
7、寻找最短路径函数(不返回原点)
该问题为NP-Hard问题,目前无最优解,这里给出我的解法。
1、创建三个数组,存到达情况、节点距离和到达顺序;
2、将出发点设置为已到达,更新节点距离,将它纳入顺序数组;
3、在for循环中scan、update、add步骤;
注:flag用于判断是否回到上一节点,若将新节点纳入顺序数组后,距离数组无更新,则再将其父节点纳入顺序数组,表回到上一节点,此处可以用stack实现,笔者想偷懒,就用数组和计数器实现了。
void find_shortest_road_dijkstra(const Graph& m,int v){
int node[m.n]={0};//是否到达
double node_distance[m.n]={INF};//节点距离
int node_sequence[100]={0};//到达顺序
for(int k=0;k<m.n;k++){
node_distance[k]=m.adjmatrix[v][k];//初始化距离
}
int cnt=1;
int v1=v;
node_sequence[0]=v;//到达顺序
node[v]=1;//起始点标记为已到达过
for(int j=0;j<m.n;j++){//一共需要进行总结点数轮
int min=INF;//先将最短边置为最大
int t;
int flag=0;//判断是否更新距离
for(int i=0;i<m.n;i++){
if(node[i]==0) continue;//已到达的点跳过
if(node_distance[i]<=min){
min=node_distance[i];
t=i;
}
}
node[t]=1;//将距上一节点最近的点纳入集合并标记为已到达
node_sequence[cnt++]=t;
for(int i=0;i<m.n;i++){//更新距离
if(node_distance[i]>=m.adjmatrix[t][i]){
node_distance[i]=m.adjmatrix[t][i];
flag=1;
}
}
if(flag==0) node_sequence[cnt++]=v;//若将该节点纳入集合后无距离更新,则返回上一节点
else v=t;
}
cout<<"从地点:"<<m.v[v1].name<<"经过校园所有地点的最短路径为:"<<endl;
for(int i=0;i<cnt;i++){
if(i=cnt-1){
cout<<m.v[node_sequence[i]].name<<endl;
}
else cout<<m.v[node_sequence[i]].name<<"-->";
}
}
8、寻找最短路径函数(返回原点)
该问题为TSP问题(旅行商问题),NP-Hard问题,无最优解,笔者也无解,遂从网上摘取该问题的近似解2-OPT算法,该算法相比其他算法如退火算法、遗传算法更适合较为简单的图。
double totalDistance(const std::vector<int>& path, const Graph& graph) {
double total = 0;
for (size_t i = 0; i < path.size() - 1; ++i) {
if (graph.adjmatrix[path[i]][path[i + 1]] != INF) { // 检查是否为INF
total += graph.adjmatrix[path[i]][path[i + 1]];
} else {
continue;
}
}
// 回到源点的距离
if (graph.adjmatrix[path.back()][path.front()] != INF) { // 检查是否为INF
total += graph.adjmatrix[path.back()][path.front()];
} else {
cout<<"没有路径"<<endl;// 处理无边存在的情况,报错
}
return total;
}
void twoOptSwap(vector<int>& path, int i, int j, const Graph& graph) {
if (i < j) {
std::swap(path[i], path[j]);
for (int k = i + 1; k < j; ++k) {
std::swap(path[k], path[k + 1]);
}
} else {
std::swap(path[i], path[j]);
for (int k = j + 1; k < i; ++k) {
std::swap(path[k], path[k - 1]);
}
}
}
// 2-opt局部搜索算法
void twoOpt(vector<int>& path, const Graph& graph) {
int n = graph.n; // 顶点数量
double bestLength = totalDistance(path, graph);
int bestIter = 0;
while (true) {
bool improved = false;
for (int i = 0; i < n - 2; ++i) {
for (int j = i + 1; j < n - 1; ++j) {
// 尝试交换
vector<int> newPath = path;
twoOptSwap(newPath, i, j, graph);
double newLength = totalDistance(newPath, graph);
if (newLength < bestLength) {
bestLength = newLength;
bestIter = 0;
path = newPath;
improved = true;
} else if (newLength == bestLength) {
// 如果长度相同,则随机选择是否保留新的路径
if (rand() % 2 == 0) {
path = newPath;
}
}
}
}
if (!improved) {
// 没有改进,结束循环
break;
}
// 如果在一定迭代次数内没有改进,也结束循环
if (++bestIter > 1000) {
break;
}
}
}
void circuit_shortest_TSP(const Graph& m, int startVertex) {
// 初始化路径,所有顶点都要访问一次
std::vector<int> path(m.n, -1); // -1 表示该位置尚未分配顶点
path[0] = startVertex; // 设置起点
int nextIndex = 1;
for (int i = 0; i < m.n; ++i) {
if (i != startVertex) { // 排除起始顶点
path[nextIndex++] = i;
}
}
// 使用2-opt算法优化路径
twoOpt(path, m);
// 输出最短TSP电路及其长度
double length = totalDistance(path, m);
std::cout << "从地点 "<<m.v[startVertex].name<<"出发的最短回路是:";
for (int vertex : path) {
std::cout << vertex << " ";
}
std::cout << "最短回路长度为: " << length << std::endl;
cout<<endl;
}
9、读入文件函数与存储文件函数
如图,非常简单。
void createGraphFromFile(Graph& graph, const string& filename) {
ifstream file(filename);
if (!file.is_open()) {
cerr << "Error opening file: " << filename << endl;
return;
}
// 读取顶点数
file >> graph.n;
if (graph.n < 0 || graph.n > numbervertex) {
cerr << "Invalid number of vertices." << endl;
file.close();
return;
}
// 初始化邻接矩阵为无穷大
for (int i = 0; i < graph.n; ++i) {
for (int j = 0; j < graph.n; ++j) {
graph.adjmatrix[i][j] = INF;
}
}
// 读取顶点信息
for (int i = 0; i < graph.n; ++i) {
file >> graph.v[i].name >> graph.v[i].info >> graph.v[i].x >> graph.v[i].y;
}
// 读取边信息
string u, v;
int weight;
while (file >> u >> v >> weight) {
// 查找顶点u和v的索引
int uIndex = -1, vIndex = -1;
for (int i = 0; i < graph.n; ++i) {
if (graph.v[i].name == u) {
uIndex = i;
}
if (graph.v[i].name == v) {
vIndex = i;
}
}
// 如果找到了顶点u和v,则设置它们的边权
if (uIndex != -1 && vIndex != -1) {
graph.adjmatrix[uIndex][vIndex] = weight;
// 对于无向图,设置反向边权
graph.adjmatrix[vIndex][uIndex] = weight;
}
}
// 顶点对之间没有边的情况保持为INF
file.close();
}
void saveGraphToFile(Graph& graph, const string& filename) {
ofstream file(filename);
if (!file.is_open()) {
cerr << "Error creating file: " << filename << endl;
return;
}
// 写入顶点数
file << graph.n<<endl
;
// 写入顶点信息
for (int i = 0; i < graph.n; ++i) {
file << graph.v[i].name << " "
<< graph.v[i].info << " "
<< graph.v[i].x << " "
<< graph.v[i].y << endl;
}
// 写入边信息
for (int i = 0; i < graph.n; ++i) {
for (int j = i + 1; j < graph.n; ++j) { // 对于无向图,只写入每条边一次
if (graph.adjmatrix[i][j] != INF) { // 如果权值不是INF,则存在边
file << graph.v[i].name << " "
<< graph.v[j].name << " "
<< graph.adjmatrix[i][j] << endl;
}
}
}
file.close();
}
注:若要通过文件修改图,请注意文件格式!
如图,第一行输入顶点个数,接下来输入顶点个数行地点及其名称、简介、坐标;剩余输入边信息。
10、主函数
注意文件名字,将源文件和你要调用的文本文件放在一个文件夹中。
int main(){
const string filename = "WUST_Map.txt"; // 文件名
Graph WUST_Map;
// 尝试从文件加载数据
createGraphFromFile(WUST_Map,filename);
welcome();
//用户菜单界面
int o;//操作数,operation
int a,b,data,v;
while(1){
menu();
cin>>o;
switch(o){
case 0:{
cout << "感谢您的使用!退出程序。" <<endl;
saveGraphToFile(WUST_Map, filename);
return 0; // 退出程序
}
case 1:{
add_node(WUST_Map);
break;
}
case 2:{
cout<<"请输入新添加边的信息:"<<endl;
show_place(WUST_Map);
cin>>a>>b;
add_edge(WUST_Map,a,b);
break;
}
case 3:{
cout<<"请输入新删除地点的信息:"<<endl;
cin>>v;
show_place(WUST_Map);
del_node(WUST_Map,v);
break;
}
case 4:{
cout<<"请输入新删除边的信息:"<<endl;
cin>>a>>b;
del_edge(WUST_Map,a,b);
break;
}
case 5:{
cout<<"校园地图是:"<<endl;
printCampusMap(WUST_Map);
show_place(WUST_Map);
break;
}
case 6:{
cout<<"校园电路最优方案是:"<<endl;
circuit_Prim(WUST_Map);
break;
}
case 7:{
show_place(WUST_Map);
cout<<"请输入要查询的地点的编号:"<<endl;
cin>>v;
search(WUST_Map,v);
break;
}
case 8:{
show_place(WUST_Map);
cout<<"请输入出发地点的编号:"<<endl;
cin>>v;
find_shortest_road_dijkstra(WUST_Map,v);
break;
}
case 9:{
show_place(WUST_Map);
cout<<"请输入源点的编号:"<<endl;
cin>>v;
circuit_shortest_TSP(WUST_Map, v);
break;
}
default:
cout << "无效的选择,请重新输入。" <<endl;
}
}
return 0;
}
结语
非常basic的一个课设,制作并不精良,希望能带来帮助;若需要相关课程设计报告或其他信息欢迎私信。