教学班级:005,项目GitHub地址:https://github.com/CapFreddy/SoftwareEngineering_HW1
项目 | 内容 |
---|---|
课程 | 2020春季计算机学院软件工程(罗杰 任建) |
作业要求 | 个人项目作业要求 |
我的课程目标 | 章握软件开发的方法论和诸多技术,与同学合作开发出好的软件 |
本次作业的作用 | 实战体验个人开发过程;掌握C++基本数据结构 |
解题思路描述
拿到题目后,第一反应是最简单的两两求交点:对于给定的$n$个几何对象,两两求交点后将交点放入集合中,最后输出集合的大小,时间复杂度为$O(n^2)$。随即发现,性能测试中几何体最多可以达到$500000$个,而最多却只有$5000000$个交点。即便在全部为直线的情况下,$500000$条直线最多产生$124,750,000,000$个交点——这说明存在大量的平行/多线交于一点。而在多线共点广泛存在的情况下,两两求交点浪费了许多计算量。
建立这样一个认识后,开始寻找优化的算法,并在Stack Overflow找到Calculate the number of intersections of the given lines的问题。虽然问题下没有给出解决的算法,却有人提到了”$n$条线段求交点“的Bentley-Ottmann算法。该算法在$n$条线段形成$k$个交点时时间复杂度为$O((n+k)\log{n})$,这意味着在$k=o(\frac{n^2}{\log{n}})$的情况下优于穷举法。算法通过维护一个结点队列及扫描线链表,以交点的产生推进,从而避免了重合结点的重复计算。”求N条线段的所有交点——Bentley-Ottmann算法“提供了一个比较简明的算法描述。
当几何体为直线时,将初始扫描线定至$x$轴,算法推进过程仍然适用。由于直线的无限性,除与$x$轴平行的(这些直线可以通过穷举法解决)之外的所有直线在任何情况下都与扫描线相交,因此算法的过程实际上简单了一些。然而实现过程中,在解决了诸多数据结构的问题后卡在了对象经过容器传递后指针丢失的问题上,考虑到附加题的扩展性便回归了原始做法。
PSP表
PSP2.1 | Stages | Estimation Time Cost(minute) | Time Consumed(minute) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | 估算任务总时长 | 30 | 30 |
Development | 开发 | ||
· Analysis | 需求分析/学习新技术 | 150 | 180 |
· Design Spec | 制作设计文档 | 30 | 30 |
· Design Review | 设计复审 | 0 | 0 |
· Coding Standard | 代码规范 | 15 | 15 |
· Design | 具体设计 | 60 | 80 |
· Coding | 具体编码 | 240 | 300 |
· Code Review | 代码复审 | 30 | 30 |
· Test | 测试 | 60 | 60 |
Reporting | 报告 | ||
· Test Report | 测试报告 | 60 | 1 |
· Size Measurement | 计算工作量 | 20 | 20 |
· Postmortem & Process Improvement Plan | 总结 | 30 | 30 |
合计 | 725 | 855 |
设计实现过程
类设置采用每种几何对象单成一类的方式,设置了直线,圆和结点类。几何对象由输入数据直接构成并放置于vector中,而后两两相交形成交点(特别地,直线和圆的相交由直线类处理)。此外,由于最开始对其他算法的尝试,将两两求交方法及扫描线方法分别封装为两个类,类内维护各自需要的数据结构。
单元测试根据几何对象之间可能的相对位置设计测试样例。由于主程序依赖于命令行输入,需要在主类中另设测试接口,并附加输出一些几何对象相关信息以协助锁定bug。测试样例的设计基本涵盖了多线共点,平行/垂直于$x$轴,直线与圆、圆与圆的三种位置关系及其组合。的确帮助找到了一些bug。
性能改进
对程序性能提升最大的改进是引入unordered_set来存储交点。通过重载结点类的==判断方法并在std中加入了计算自定义结点类的hash值方法实现。明显降低了计算时间。
class Node
{
public:
// overload operator
bool operator ==(const Node& obj) const
{
return abs(m_x - obj.m_x) < 1e-6 && abs(m_y - obj.m_y) < 1e-6;
}
private:
double m_x, m_y;
};
// add difinition of hash code for Node class
namespace std
{
template<>
struct hash<Node>
{
size_t operator()(const Node& obj) const
{
return hash<long long>()(llround(obj.getX())) + hash<long long>()(llround(obj.getY()));
}
};
}
调整数据结构后,算法在随机生成的100000条数据中的表现如下,可以看到,直线之间求交点是占用CPU时间最长的函数。对此的一个优化方法是在读入数据时将直线斜率(若存在)计算好,以避免斜率的重复计算。
代码说明
项目的核心部分是几何对象交点的计算,相关代码如下:
· 直线与直线的交点通过联立二元一次方程组求解。
vector<Node> IntersectLine(Line line)
{
double x, y;
vector<Node> intersections;
if (!parallels(line))
{
if (this->m_vertical)
{
x = m_x1;
y = line.m_k * ((double)m_x1 - line.m_x1) + line.m_y1;
}
else if (line.m_vertical)
{
x = line.m_x1;
y = m_k * ((double)line.m_x1 - m_x1) + m_y1;
}
else
{
x = (double)(m_k * m_x1 - line.m_k * line.m_x1 + line.m_y1 - m_y1) / (m_k - line.m_k);
y = (double)m_k * (line.m_k * ((double)m_x1 - line.m_x1) + (double)line.m_y1 - m_y1) / (m_k - line.m_k) + m_y1;
}
intersections.push_back(Node(x, y));
}
return intersections;
· 直线与圆、圆与圆皆依赖圆方程建立二次方程求解。
vector<Node> IntersectCircle(Circle circle)
{
vector<Node> intersections;
int baX = m_x2 - m_x1;
int baY = m_y2 - m_y1;
int caX = circle.getX() - m_x1;
int caY = circle.getY() - m_y1;
int a = baX * baX + baY * baY;
int bBy2 = baX * caX + baY * caY;
int c = caX * caX + caY * caY - circle.getR() * circle.getR();
double pBy2 = (double)bBy2 / a;
double q = (double)c / a;
double disc = pBy2 * pBy2 - q;
if (disc >= 0)
{
double tmpSqrt = sqrt(disc);
double abScalingFactor1 = -pBy2 + tmpSqrt;
double abScalingFactor2 = -pBy2 - tmpSqrt;
intersections.push_back(Node(m_x1 - baX * abScalingFactor1, m_y1 - baY * abScalingFactor1));
if (disc != 0)
{
intersections.push_back(Node(m_x1 - baX * abScalingFactor2, m_y1 - baY * abScalingFactor2));
}
}
return intersections;
}
vector<Node> Intersect(Circle circle)
{
double d, a, h, x2, y2, x3, y3, x4, y4;
vector<Node> intersections;
d = sqrt((circle.m_x - m_x) * (circle.m_x - m_x) + (circle.m_y - m_y) * (circle.m_y - m_y));
if (intersects(circle, d))
{
a = ((double)m_r * m_r - (double)circle.m_r * circle.m_r + d * d) / (2 * d);
h = sqrt((double)m_r * m_r - a * a);
x2 = m_x + a * ((double)circle.m_x - m_x) / d;
y2 = m_y + a * ((double)circle.m_y - m_y) / d;
if (d < 1e-6)
{
Node intersection(x2, y2);
intersections.push_back(intersection);
}
else
{
x3 = x2 + h * ((double)circle.m_y - m_y) / d;
y3 = y2 - h * ((double)circle.m_x - m_x) / d;
x4 = x2 - h * ((double)circle.m_y - m_y) / d;
y4 = y2 + h * ((double)circle.m_x - m_x) / d;
Node intersection1(x3, y3);
Node intersection2(x4, y4);
intersections.push_back(intersection1);
intersections.push_back(intersection2);
}
}
return intersections;
}
收获/后继目标
本次作业中对算法的尝试卡在了容器传值时指针发生的变化,后续计划更加详尽地了解C++运行时内存分配以更自如地运用指针。同时多积累数据结构使用技巧,提高编码效率。