4 校园导航
4.1 需求规格说明
【问题描述】
一个学校平面图,至少包括10个以上的场所,每个场所带有编号、坐标、名称、类别等信息,两个场所间可以有路径相通,路长(耗时)各有不同。要求读取该校园平面图,查询各个场所信息,找出从任意场所到达另一场所的最短路径(最佳路径)。
【基本要求】
校园平面图用图数据结构表达,采用指令或菜单方式操作,实现场所查询和路径求解。
【提高要求】
编写图形用户界面程序,使用交互方式:1)绘制校园平面图,并加以存储;2)点选查询场所信息;3)点选起点和终点,显示求得的最佳路径。
【测试数据】
自行设计校园平面图,用数据文件存储,格式自定。
4.2 总体分析与设计
(1)设计思想
①存储结构
在解决校园导航的问题中,这里程序的图结构使用了殷人昆数据结构中的图结构作为模板,定义了顶点的数据类型 VertexData 结构体。指定模板的顶点数据类型为 VertexData,边上的权值类型为 float。定义使用该数据类 型的模板别名为 Edgef、Vertexf 和 Graphf。接着实现了 GraphAdjList 类,派生自 Graphf 类, 增加了相应函数方便快速查找。VertexData 结构体存储了顶点类型(可分为场所和路径,场所 拥有名称和类型而路径没有),索引,x 坐标、y 坐标,名称、类别等数据。此外,定义了结 构体 DjikstraPathsAndCost 负责存储顶点所对应最短路径的权值和所经过路径。
②主要算法思想
校园导航的核心数据结构为图结构,顶点表示校园中场所、或者路径点,边表示场所或者路径点之间的道路。每条边之间都有相应的权值,代表边的同行便利成程度。求解最短路径的核心算法为Dijkstra算法。该算法的核心思想为:
(1)指定一个起点顶点D。引进两个数组S和U。其中S的作用是记录已经求出的最短路径的顶点和经过路径以及距离顶点D的最短距离,U的作用是记录还未求出最短路径的顶点和经过路径以及距离顶点D的距离。
(2)初始化S和U集合。最初,S集合中只包含顶点D,U集合中包含除了顶点D以外的所有顶点。如果U中顶点和D不相邻,则距离为无穷大。
(3)从U集合中取出一个离顶点D距离最短的顶点K,将其加到数组S中。同时从U集合中移除顶点K。
(4)根据S集合中的路径更新U集合中每个顶点距离顶点D的最短距离和相应路径。
(5)重复(3)、(4)操作直到U集合为空。
(2)设计表示
校园导航构建的UML类图如图4.2-1所示。
图4.2-2 程序UML类图
类Edgef、Vertexf和Graphf分别为Edge、Vertex、Graphlnk,均来自殷人昆数据结构中的图结构。
类CampusNavigationThread为算法的核心实现类。该类包含一个图GraphAdjList,函数LoadGraph负责读取图数据,SaveGraph负责保存平面图数据。核心算法函数为Dijkstra,接受一个起点在图中的索引位置,一个映射引用作为返回值。
类CampusNavigation为界面类,派生自QMainWindow,实现了相应的槽函数和绘制点、绘制、线等函数。核心为使用了QgraphicSence和CampusNavigationView进行显示。
类CampusNavigationView为视图类,派生自QGraphicsView,重写了其鼠标交互函数,负责获取鼠标在视图坐标系中的坐标并传递给主窗口。
(3)详细设计表示
校园导航构建的程序流程图如图4.2-2所示。
图4.2-2 校园导航程序流程图
该程序的流程步骤如下所示:
1.初始化:声明一个数组dis来保存原点到各个顶点的最短距离,将起始节点的距离设为0,对于顶点s存在能直接到达的边(s,m),则把dis[m]设为w(s,m),同时把其他所有s不能直接到达的顶点的路径长度设为无穷大。创建一个空集合T,用于存储已经找到最短路径的节点。初始时,集合T只有起点S。
2.选择最近的节点:从未包含在T中的节点中,从dis数组选择最小值,即选择距离最短的节点,然后将其添加到T中。
3.更新距离:检查新添加的节点的所有邻接节点。如果通过新添加的节点到达邻接节点的距离小于比源点直接到达的距离,那么就更新这个距离。
4.重复步骤2和3:重复步骤2和3,直到集合T包含了图的所有节点。
迪杰斯特拉算法的优点是能够找到从起始节点到所有其他节点的最短路径,而且算法的时间复杂度相对较低。然而,需要注意的是,这个算法不能处理存在负权边的图。
4.3 编码
【问题1】:对殷人昆数据结构中图模板的理解与应用以及修改?
【解决方式】:在阅读了殷人昆数据结构中图模板的源代码后,程序派生并修改了其图类。为了更加贴近校导航的真实场景,程序尝试定义场所点Site和路径点RoadNode,场所是具有实际意义的地点,路径点是进行同行的点。决定使用自定义性高的json数据表达校园平面图,分别存储场所、路径点以及边。在交互功能的实现中,交互过程中需要将窗口坐标转换为视图坐标,因此需要重写QgraphicView类。
【问题2】:为了提升用户体验,本导航精心设计了多种鼠标事件。然而,在某些情况下,这些事件可能会出现冲突,导致无法正常响应。
【解决方式】:在处理鼠标交互事件时,可以采用状态模式设计来解决不同状态下鼠标事件冲突的问题。状态模式设计包括编辑模式和导航模式两种状态。在编辑模式下,可以处理鼠标的点击、移动、双击事件,而在导航模式下,则可以处理鼠标的点击、移动事件,且拥有不同的响应。通过在不同的状态下处理不同的鼠标事件,可以有效地避免事件冲突的问题,用户使用更加友好。
【问题2】:QPainter绘制时点与线之间压盖,无法清除画布已经绘制的事件。
【解决方式】:首先,需要注意绘制的顺序。在绘制点与线时,需要先绘制线,再绘制点,这样可以保证点不会压在线上而导致无法清除已经绘制的事件。如果已经出现了压盖的情况,可以通过重绘事件来解决问题。重绘事件时需要注意保持绘制的顺序一致,否则可能会出现新的问题。
4.4 程序及算法分析
①使用说明
打开程序后,即可出现初始化程序样式,如图4.4-1所示。
图4.4-1 初始化程序
这里可以点击“打开文件”,会弹出一个对话框选择文件路径,加载准备好的json数据。将将鼠标放置到红色个实心圆(场所点)上在左下角显示场所数据;点击“添加顶点”,在地图上点击即可添加顶点。点击后会弹出对话框选择顶点类型并输入相应数据。可以选择添加场所点或者路径点;点击“添加边”,选中两个顶点,若成功选中,则会弹出对话框输入权重;如图4.4-2所示。
图4.4-2 详细操作流程
此外,程序还支持进行最短路径的查询操作,当点击“求解最短路径”时,在图上选中两个顶点,系统会自动求解两点之间的最短路径并使用蓝色线进行绘制。如图4.4-3所示。
图4.4-3 求解最短路径
此外,程序还支持将输出的文件进行保存,当点击“保存”时,会弹出对话框选择保存路径,绘制好的平面图会以json格式保存,如图4.4-4所示。
图4.4-4 保存文件
同时,为了考虑到界面的交互效果,这里还支持放大和缩小操作,均由QAction控件控制,如图4.4-5所示。
图4.4-5 支持放大缩小
4.5 小结
本题的核心是求解最短路径,这是校园导航系统的关键所在。在众多求解最短路径的算法中,我选择了Dijkstra算法。原因在于此次题目要求的是校园导航,这是一个单源点正权图的问题,而Dijkstra算法正是针对这类问题设计的。它能够大大降低时间复杂度,提高求解效率。本导航系统以校园地图为基础,可以加载预先准备好的校园场所点和道路拐点数据。同时,用户也可以根据自己的需求编辑线路,使得系统的灵活性和拓展性得到了极大的提升。这使得我们能够真实模拟校园场景,包括各个路口的拐点。从而完成最短路径规划,为校园内的用户提供便捷、高效的导航服务。
为了提供更好的用户体验,本系统不仅具备编辑、导航功能,还注重用户交互体验的细节。多种鼠标事件的支持,对鼠标悬停、点击等行为进行细致处理,使得系统能够及时响应用户的操作,使得用户能够以更加自然、直观的方式与系统进行交互,极大地提升了用户的使用体验。多种信息展示方式,用户可以轻松获取校园场所点的详细信息,更好地了解校园的各项设施和服务。
但是,本题使用的算法需要遍历所有的节点,并在每一步都找到未访问的节点中距离最短的节点。如果节点的数量非常大,这个函数可能会比较慢。未来将采用更高效的实现,例如使用优先队列来存储未访问的节点。
4.6 附录
//实现点的插入
// 顶点数据结构
struct VertexData
{
VertexType type = VertexType::None; // 顶点类型,默认为None
int index = -1; // 数据索引
float x = 0.0f; // 顶点的X坐标
float y = 0.0f; // 顶点的Y坐标
string name = ""; // 顶点名称
string category = ""; // 分类信息
VertexData() {} // 默认构造函数
VertexData(int) {} // 带参数构造函数(空实现)
// 比较操作符重载,用于判断两个顶点数据是否相等
bool operator==(const VertexData& other) const
{
return type == other.type
&& index == other.index
&& x == other.x
&& y == other.y
&& name == other.name
&& category == other.category;
}
};
//实现线的插入
// 图的邻接表表示类,继承自Graphlnk
class GraphAdjList : public Graphf
{
public:
GraphAdjList(int sz) :Graphf(sz) {}; //构造函数
~GraphAdjList() {}; //析构函数
// 根据数据索引和类型获取顶点在邻接表中的位置
int getVertexPosByDataIndex(int _index, VertexType type)
{
for (int i = 0; i < numVertices; i++)
{
if (NodeTable[i].data.type != type)
continue;
int index = NodeTable[i].data.index;
if (index == _index)
return i;
}
return -1;// 未找到对应顶点
}
// 获取所有边的信息,返回一个包含起点、终点和权值的元组列表
vector<tuple<int, int, float>> getEdges()
{
std::set<std::pair<int, int>> visitedEdges; // 用于跟踪已访问的边
std::vector<std::tuple<int, int, float>> edges; // 存储边的信息
for (int i = 0; i < numVertices; ++i)
{
Edgef* edge = NodeTable[i].adj; // 当前顶点的邻接边表
while (edge != nullptr)
{
int u = i; // 起点
int v = edge->dest; // 终点
if (visitedEdges.find({ v, u }) == visitedEdges.end())
{ // 如果这条边未被访问过
visitedEdges.insert({ u, v }); // 标记为已访问
edges.push_back({ u, v, edge->cost });
}
edge = edge->link; // 遍历下一条边
}
}
return edges;
}
// 根据索引获取指定顶点的指针
const Vertexf* getVertex(int index)
{
if (index < 0 || index >= numVertices)
return nullptr;
return &NodeTable[index];
}
// 获取所有站点类型的顶点
const vector<Vertexf*> getAllSiteVertexs()
{
vector<Vertexf*> vertexs;
for (int i = 0; i < numVertices; i++)
{
if (NodeTable[i].data.type == VertexType::Site)
{
vertexs.push_back(&NodeTable[i]);
}
}
return vertexs;
}
// 获取所有路节点类型的顶点
const vector<Vertexf*> getAllRoadNodeVertexs()
{
vector<Vertexf*> vertexs;
for (int i = 0; i < numVertices; i++)
{
if (NodeTable[i].data.type == VertexType::RoadNode)
{
vertexs.push_back(&NodeTable[i]);
}
}
return vertexs;
}
// 获取站点数量
int NumberOfSites()
{
int count = 0;
for (int i = 0; i < numVertices; i++)
{
if (NodeTable[i].data.type == VertexType::Site)
count++;
}
return count;
}
// 获取路节点数量
int NumberOfRoadNodes()
{
int count = 0;
for (int i = 0; i < numVertices; i++)
{
if (NodeTable[i].data.type == VertexType::RoadNode)
count++;
}
return count;
}
// 获取图的边界矩形
QRectF getBoundingRect()
{
float minX = std::numeric_limits<float>::max();
float minY = std::numeric_limits<float>::max();
float maxX = std::numeric_limits<float>::min();
float maxY = std::numeric_limits<float>::min();
for (int i = 0; i < numVertices; i++)
{
float x = NodeTable[i].data.x;
float y = NodeTable[i].data.y;
if (x < minX)
minX = x;
if (x > maxX)
maxX = x;
if (y < minY)
minY = y;
if (y > maxY)
maxY = y;
}
return QRectF(minX, minY, maxX - minX, maxY - minY);
}
};
//实现Djikstra算法
bool CampusNavigationThread::Dijkstra(int startVertexIndex, map<int, DjikstraPathsAndCost>& S_vertexPosAndCost)
{
if (startVertexIndex < 0 || startVertexIndex >= mGraph->NumberOfVertices())
return false;
map<int,DjikstraPathsAndCost> U_vertexPosAndCost;
// 初始化U集合
for (int i = 0; i < mGraph->NumberOfVertices(); i++)
{
if(i==startVertexIndex) //起点不加入U集合
continue;
float cost = mGraph->getWeight(startVertexIndex, i);
DjikstraPathsAndCost pathAndCost;
if(cost != -1) // 顶点I和起点之间有边
{
pathAndCost.cost = cost;
}
else
{
pathAndCost.cost = std::numeric_limits<float>::max();
}
pathAndCost.pathIndex.push_back(i);
U_vertexPosAndCost.insert(pair<int, DjikstraPathsAndCost>(i, pathAndCost));
}
// 初始化S集合
DjikstraPathsAndCost startPathAndCost;
startPathAndCost.cost = 0;
startPathAndCost.pathIndex.push_back(startVertexIndex);
S_vertexPosAndCost.insert(pair<int, DjikstraPathsAndCost>(startVertexIndex, startPathAndCost));
while (!U_vertexPosAndCost.empty())
{
// 寻找U集合中权值最小的顶点I
auto minPos = min_element(U_vertexPosAndCost.begin(), U_vertexPosAndCost.end(),
[](const pair<int, DjikstraPathsAndCost>& a, const pair<int, DjikstraPathsAndCost>& b) { return a.second.cost < b.second.cost; });
S_vertexPosAndCost.insert(*minPos); // 将权值最小的顶点I加入S集合
float oldCost = minPos->second.cost; // 起点到顶点I的距离
vector<int> oldPath = minPos->second.pathIndex; // 起点到顶点I的路径
// 从U集合中移除顶点I
int minIndex = minPos->first;
U_vertexPosAndCost.erase(minPos);
// 更新U集合中的各顶点到起点的距离
for (auto& K_pair : U_vertexPosAndCost)
{
int i = K_pair.first;
float minCost = K_pair.second.cost;
float newCost = mGraph->getWeight(minIndex, i); // 顶点K和顶点I之间的距离
if (newCost == -1) // 顶点K和顶点I之间没有边,保持原来的距离
continue;
newCost += oldCost; // 顶点K到起点的距离
if (newCost < minCost)
{
K_pair.second.cost = newCost;
K_pair.second.pathIndex = oldPath;
K_pair.second.pathIndex.push_back(i);
}
}
}
return true;
}
//实现Djikstra算法
//将图的数据保存到JSON文件
bool CampusNavigationThread::SaveGraph(QString path)
{
QJsonObject root;
// 保存Sites数据
QJsonArray sitesArray;
const vector<Vertexf*> AllSites = mGraph->getAllSiteVertexs();
for (const auto& site : AllSites)
{
QJsonObject siteObject;
siteObject["Index"] = site->data.index;
siteObject["x"] = site->data.x;
siteObject["y"] = site->data.y;
siteObject["Name"] = QString::fromStdString(site->data.name);
siteObject["Category"] = QString::fromStdString(site->data.category);
sitesArray.append(siteObject);
}
root["Sites"] = sitesArray;
// 保存 RoadNodes 数据
QJsonArray roadNodesArray;
const vector<Vertexf*> AllRoadNodes = mGraph->getAllRoadNodeVertexs(); // 获取所有路节点的顶点数据
for (const auto& roadNode : AllRoadNodes)
{
QJsonObject roadNodeObject;
roadNodeObject["Index"] = roadNode->data.index;
roadNodeObject["x"] = roadNode->data.x;
roadNodeObject["y"] = roadNode->data.y;
roadNodesArray.append(roadNodeObject);
}
root["RoadNodes"] = roadNodesArray;
// 保存Edges数据
QJsonArray edgesArray;
vector<tuple<int, int, float>> edges = mGraph->getEdges();
for (const auto& edge : edges)
{
QJsonObject edgeObject;
edgeObject["FromType"] = QString::fromStdString(getVertexTypeAsString(mGraph->getValue(std::get<0>(edge)).type));
edgeObject["FromIndex"] = mGraph->getValue(std::get<0>(edge)).index;
edgeObject["ToType"] = QString::fromStdString(getVertexTypeAsString(mGraph->getValue(std::get<1>(edge)).type));
edgeObject["ToIndex"] = mGraph->getValue(std::get<1>(edge)).index;
edgeObject["Cost"] = std::get<2>(edge);
edgesArray.append(edgeObject);
}
root["Edges"] = edgesArray;
// 将图数据保存为JSON数据
QFile file(path);
if (file.open(QIODevice::WriteOnly))
{
QJsonDocument jsonDoc(root);
file.write(jsonDoc.toJson());
file.close();
return true;
}
else
{
return false;
}
}
项目源代码:
Data-structure-coursework/4 at main · CUGLin/Data-structure-courseworkhttps://github.com/CUGLin/Data-structure-coursework/tree/main/4