2 凸包求解
2.1 需求规格说明
【问题描述】
凸包是一种基础的几何结构,在地理信息科学领域广泛应用。给出一组平面上的点,构造出对应的凸包,并依次输出极点编号。
【输入格式】
第一行即输入点的总数n,接下来n行依次给出各点的横纵坐标,横坐标与纵坐标间用空格分隔,如样例所描述。
【输出格式】
依次输出各个极点的对应编号(编号从1开始,而非0)。
可基于界面编程将凸包可视化。要求算法代码和界面代码分离。
2.2 总体分析与设计
(1)设计思想
①存储结构
在凸包求解的设计中,我采用了以下存储结构:
1.点集(Vector):使用std::vector<QPointF>来存储输入的点集,其中QPointF是一个包含x和y坐标的结构体,方便表示二维平面上的点。
2.基准点(QPointF):定义了一个basePoint成员变量,用于存储在Y轴上最低的点,作为后续极角排序的基准。
3.凸包点索引(Vector):使用std::vector<int>来存储凸包点的索引,方便最终输出凸包点的编号。
4.凸包点集(Vector):最终构成凸包的点集也使用std::vector<QPointF>存储,包含了构成凸包的所有顶点。
这里使用标准模板库(STL)中的vector可以让我们更方便地使用各种算法和操作,例如排序、查找、遍历等。通过使用引用,我可以直接在原始的场景上进行操作,而不需要复制整个场景,这既提高了效率(特别是对于密集的大量点集情况时而言),也允许多个函数或对象共享和操作同一个场景。
②主要算法思想
算法的主要思想基于Graham扫描算法,该算法分为以下几个步骤:
1.寻找基准点:首先在所有点中找到Y坐标最小的点作为基准点,如果存在多个这样的点,则取X坐标最小的点。
2.极角排序:以基准点为参考,计算其他点相对于基准点的极角(即与正x轴的夹角),并将所有点按照极角从小到大的顺序进行排序。如果极角相同,则按照距离基准点的距离进行排序。
3.构建凸包:使用一个栈来维护凸包的顶点。遍历排序后的点集,对于每个点,使用叉积来判断其与栈中最后两个点构成的向量是否为逆时针方向。如果是,则该点在凸包上,将其添加到栈中;如果不是,则栈中的最后一个点不再是凸包的一部分,从栈中移除,直到满足逆时针条件。
4.处理共线情况:在处理过程中,如果三个点共线,则忽略中间的点,只保留首尾两点。
5.输出结果:最终,栈中剩余的点即为凸包的顶点,按照索引输出这些点的编号。
通过这种设计,我们能够有效地求解凸包问题,并且算法的时间复杂度为O(n log n),其中n是点集中点的数量。这种算法在处理大规模数据时具有较好的性能。
(2)设计表示
实现凸包求解的程序UML类图如图2.2-1所示。
图2.2-1 程序UML类图
1.初始化:Graham类的构造函数初始化基准点basePoint。
2.距离和叉积计算:distanceSquared和crossProduct方法分别用于计算两点间的距离平方和叉积。
3.排序:compareY和comparePolar方法用于实现点的排序逻辑。
4.凸包计算:computeConvexHull方法是计算凸包的核心,它通过栈操作来确定最终的凸包顶点。
5.结果展示:showHullIndices方法用于展示凸包点的索引,以便于验证和展示算法结果。
(3)详细设计表示
程序的UML类图如图2.2-2所示
图2.2-1 凸包求解程序流程图
该程序的流程步骤如下所示:
1.输入处理:程序首先接收一组平面上的点作为输入,这些点的横纵坐标通过标准输入给出。
2.基准点确定:通过compareY函数,我们找到Y坐标最小的点作为基准点。
3.极角排序:所有点相对于基准点进行极角排序,使用comparePolar函数实现。
4.栈操作:通过栈来维护凸包的顶点,利用叉积来判断点的添加或移除。
5.输出结果:最终,凸包的顶点被输出,这些顶点即为所求的凸包。
2.3 编码
【问题1】:随机生成点时,如有重复点或共线问题会造成凸包向内凹陷或计算失败
【解决方式】:在随机生成点时,需要进行判断,查看新生成的点是否与之前的点重复。一旦发现重复点,则立即重新随机生成,直到没有重复点为止。这样可以确保生成的点不会造成凸包向内凹陷的情况。
【问题2】:极角相同的情况下如何对两个点的顺序进行排序?
【解决方式】:在极角度相同的情况下,我们需要对两个点的顺序进行排序。一种方法是,如果近的点序号在前,那么远的点方向一定会使得近的点形成凹形,因此需要将近处的点作为内部点踢走。而如果远的点序号在前,那么一系列点会出现共线的情况。如果让程序选择保留共线的点(也就是可能凸包的一条边上有多个点,这些点都进行保留而不是提出),则也可以形成凸包。但在第一个与最后一个枚举点时会有比较麻烦的问题。因此,一般选择近的点在前,若出现共线的线段,则踢出,这样确保选出所有凸包的顶点。虽然本题中是需要输出凸包边上的所有点的,那么我们可以考虑先利用凸包处理出所有的拐点,然后按照极角枚举,将刚好处于凸包边上共线点也一起加入。
2.4 程序及算法分析
①使用说明
打开程序后,即可出现作者预设的坐标轴样式,如图2.4-1所示。
图2.4-1 初始化程序
接下来,点击左上角最左侧“开始编辑”按钮即可直接在GraphicsView中使用鼠标左键点击生成点,并且点击的每个点都会被记录下其坐标信息显示到左侧数据表格中,同时点击的每个点都会按序编号,如图2.4-2所示。
图2.4-2 编辑界面
接下来,点击相应的第二个按钮“结束编辑”,即可停止编辑,此时由于写入了mouseMoveEvent鼠标移动事件,故鼠标的同步移动同时也会显示到相应的状态栏中被读取到。而当点击第三个按钮“生成凸包”时即会启用凸包算法生成凸包,同时会以MessageBox的形式弹窗告知凸包信息,如图2.4-3所示。
图2.4-3 生成凸包样式
点击【OK】后同步生成的凸包结果即会可视化界面中,如图2.4-4所示。
图2.4-4 凸包生成结果
同样,这里还可以接续点击“开始编辑”添加点,如图2.4-5所示。
图2.4-5 继续添加点
接下来,点击“结束编辑”,并点击“生成凸包”,此时程序会将添加后的点和此前的点一并进行凸包的运算,并输出生成结果,同时将输出结果可视化展示到界面中,如图2.4-6所示。
图2.4-6 生成新的凸包结果
2.5 小结
程序在获取数据点时,可以通过读取文件或利用随机函数来生成随机点,从而满足多种变换的凸包问题,也展现了该算法的稳定性和通用性,同时也为后续的TIN构建使用凸包提供了极大的便利。为了完成极角排序,本程序具体实现依赖于简单冒泡排序算法。此算法不仅能提高了排序效率,且编写也较为简洁易懂。通过这种方式,程序能迅速确定数据点的位置和方向,从而精确构建凸包。但是排序算法仍可以继续优化,采用更稳定、时间复杂度更低的排序算法。在处理凸包边的计算时,程序利用了栈的特性来存储构成凸包的边顶点。实现进栈、退栈,以及对栈顶元素的操作存储凸包顶点。
为增强可视化和用户体验,程序在屏幕显示时添加了坐标系的横纵坐标轴。这不仅有助于观察数据点的分布,还能提高数据的可视化效果。本程序还支持重复打开文件和多次生成凸包,使得用户可以更加灵活地处理数据。主窗口的自动缩放功能,自动延展坐标轴,使用户体验更舒适。在程序的后续改进中,应注意保持命名规范,确保代码的清晰度和可维护性。
2.6 附录
//求解凸包算法
Graham::Graham() {}
double Graham::distanceSquared(const QPointF& a, const QPointF& b) {
return (a.x() - b.x()) * (a.x() - b.x()) + (a.y() - b.y()) * (a.y() - b.y());
}
double Graham::crossProduct(const QPointF& O, const QPointF& A, const QPointF& B) {
return (A.x() - O.x()) * (B.y() - O.y()) - (A.y() - O.y()) * (B.x() - O.x());
}
bool Graham::compareY(const QPointF& a, const QPointF& b) {
return (a.y() > b.y()) || (a.y() == b.y() && a.x() < b.x());
}
bool Graham::comparePolar(const QPointF& a, const QPointF& b) {
double cp = crossProduct(basePoint, a, b);
if (cp == 0) { // 共线时按距离排序
return distanceSquared(basePoint, a) < distanceSquared(basePoint, b);
}
return cp > 0; // 极角从小到大排序
}
void Graham::showHullIndices(const vector<int>& indices) {
QString indicesStr;
for (int index : indices) {
indicesStr += QString::number(index+1) + " ";
}
QMessageBox::information(nullptr, "Convex Hull Indices", "Indices of Convex Hull Points: \n" + indicesStr);
}
vector<QPointF> Graham::computeConvexHull(vector<QPointF>& points) {
int n = points.size();
if (n <= 2) return points;
// 第一步:找到基准点
basePoint = *min_element(points.begin(), points.end(), compareY);
// 第二步:按极角排序
sort(points.begin(), points.end(), [this](const QPointF& a, const QPointF& b) {
return comparePolar(a, b);
});
// 第三步:使用栈计算凸包
vector<QPointF> hull;
vector<int> hullIndices; // 保存凸包点的索引
for (size_t i = 0; i < points.size(); ++i) {
const auto& point = points[i];
while (hull.size() >= 2 &&
crossProduct(hull[hull.size() - 2], hull[hull.size() - 1], point) <= 0) {
hull.pop_back(); // 移除不合法的点
hullIndices.pop_back();
}
hull.push_back(point); // 添加当前点到凸包
hullIndices.push_back(static_cast<int>(i));
}
// 显示凸包点的序号
showHullIndices(hullIndices);
return hull;
}
项目源代码: