个人项目作业
个人项目作业
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2020春季计算机学院软件工程(罗杰 任健) |
教学班级 | 005 |
项目地址 | IntersectProject |
PSP 表格记录
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 15 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 360-540 | 600 |
Development | 开发 | - | - |
· Analysis | · 需求分析 (包括学习新技术) | 30 | 60 |
· Design Spec | · 生成设计文档 | 20 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | - | - |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 5 |
· Design | · 具体设计 | 20 | 60 |
· Coding | · 具体编码 | 120 | 240 |
· Code Review | · 代码复审 | 30 | 20 |
· Test | · 测试(自我测试,修改代码,提交修改) | 20 | 40 |
Reporting | 报告 | 20 | 60 |
· Test Report | · 测试报告 | 20 | 40 |
· Size Measurement | · 计算工作量 | 20 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 40 |
合计 | 345 | 635 |
一开始对题目思考很不全面,会有某些情况未考虑到,或是手误写错代码导致结果错误,实际耗时更多。
解题思路描述
题目要求求出交点个数,因此一开始想到的办法偏重于个数。比如,两条直线不平行必相交,会有一个交点;两个圆,直线和圆的情况可能会产生0-2个交点。但是,这种没考虑具体点的想法,会出现交点重合,重复计算个数的情况,所以还是需要考虑具体的交点坐标是多少。这样就必须计算出坐标,分为三种情况:直线和直线,圆和圆,直线和圆。计算一条直线(圆)和其他所有直线(圆)的交点;计算每条直线和所有圆的交点。最终计算出的不重复的交点坐标的个数就是交点的个数。
上网查了一些关于直线相交,圆相交的资料,没查到什么太好的办法,感觉都挺复杂的,自己也没想到。于是推导出了直线与直线相交求交点的公式,其余情况比较复杂,有如下参考:
这样做复杂度应该会很高,但是目前没有想到什么更好的办法。有过运用矩阵的想法,也许会降低一些运算量,但没有想清楚应该怎样使用,究竟能否使用也说不清,只是感觉这种问题和矩阵有莫名的联系。另外,N条直线相交最多产生 \(\frac{N(N-1)}{2}\) 个交点,这样看好像本身复杂度也挺高的。但是,通过给的测试范围能够看出,一定存在很多交点重合的情况,如何才能减少不必要的运算而降低复杂度,性能问题仍然值得思考。
设计实现过程
首先,根据集合对象创建类,直线Line
类及圆Circle
类,同时应该有基础单位点Point
类。但是,根据Point
类创建对象时,就算是两个点坐标完全一样,也会被认为是两个对象,这和我的想法有些出入,并且开始写时这个类也只负责记录某点的两个坐标,于是删去(看了同学写的博客,感觉如果保留下来这个类,添加一些方法也是不错的,比如计算一下两点的距离,两点构成直线的斜率和截距,可以让代码逻辑性更强,看起来也比较好看)。
对于直线类和圆类,需要相应的构造函数。直线需要计算出斜率k和截距b,圆根据输入直接存为圆心坐标及半径。在直线类中,应该有属于直线与直线相交的计算函数;在圆类中,应该有圆与圆的计算函数;直线与圆相交的计算函数随便放进一个类(根据我的代码放在圆类里面比较方便)。
关于单元测试
首先是一些关于特殊数据的手写样例。如直线斜率不存在,为0,两直线平行时;两圆内切,外切,相离时;直线与圆相切,相交,相离时。这种数据可以通过在GeoGebra
绘制并能大体看出结果。同时输出直线的斜率和截距,交点坐标来检查正确性。
另外是用大量的随机数据进行测试,专门写了一个intersect_test.cpp
进行测试。在构造直线和圆时随机生成了一些数据,后面的操作没太大变化。
性能分析
制造1000个随机数据,CPU使用如上图,能看出来占比最多的基本是关于自带的Tree
和求交点的几个函数,Tree
应该与set容器的实现方式RBTree有关,求交点没有办法避免,但to_string
函数也占比较多,这两点也许可以改进。关于to_string
函数,这一点目前来说是我认为比较必要的操作,考虑优化一下set容器。set容器是通过RBTree来实现有序,从CPU分析中也能看出来,但实际上我们不需要set有序,只需要用它存储一下已求出的交点坐标即可,查阅资料发现有一种unordered_set
容器,更换后性能如下:
std::Tree
这个东西已经没了,速度也比原先快了不少。现在主要是转换字符串方法占比较多,还需要进一步考虑。
一点改进
用set容器存储字符串虽然方便,但是to_string
调用太多次太费时间,重拾遗弃的Point
类,并且重载比较函数。需要注意的是,重载的比较函数主要就是多处理一下精度问题。否则会出现这种情况:
看起来是同一个点,其实在小数点后面十几位之后是不同的,如果输出的时候控制一下小数点位数是可以看出来的,但是直接输出就会是上面这种情况。这样改了以后1000个随机数据快了不少,分析如下:
主要问题还是RBTree的构建。如果坚持使用unordered_set
容器,需要重写Hash函数和等价准则,万一写错了就会很麻烦,所以感觉还是选择使用set。
代码说明
最关键的代码就是求交点的部分,如上所说,直线和直线相交比较容易推导出公式,涉及到圆的比较复杂。根据上面的参考得到如下代码,首先是两圆求交点部分:
void intersect_C(Circle otherCircle) {
double center_d = sqrt((x - otherCircle.x) * (x - otherCircle.x)
+ (y - otherCircle.y) * (y - otherCircle.y));
if (center_d != 0) {
double a = (r * r - otherCircle.r * otherCircle.r + center_d * center_d) / (2 * center_d);
double x0 = x + a / center_d * (otherCircle.x - x);
double y0 = y + a / center_d * (otherCircle.y - y);
if ((center_d == r + otherCircle.r) || (center_d == abs(r - otherCircle.r))) {
nodes.insert(to_string(trans(x0)) + "," + to_string(trans(y0)));
}
else if ((center_d > abs(r - otherCircle.r)) && (center_d < r + otherCircle.r)) {
double h = sqrt(r * r - a * a);
double x1 = x0 - h / center_d * (otherCircle.y - y);
double y1 = y0 + h / center_d * (otherCircle.x - x);
double x2 = x0 + h / center_d * (otherCircle.y - y);
double y2 = y0 - h / center_d * (otherCircle.x - x);
nodes.insert(to_string(trans(x1)) + "," + to_string(trans(y1)));
nodes.insert(to_string(trans(x2)) + "," + to_string(trans(y2)));
}
}
}
计算出圆心距,不为0时考虑内切,外切,相交的情况,把数学公式翻译成代码,思考起来比较容易,最后把计算出来的点加到set中。
然后是圆与直线求交点部分:
void intersect_L(Line line) {
if (line.k == INFINITY) {
double d = abs(line.b - x);
if (d < r) {
double offset = sqrt(r * r - d * d);
double y1 = y + offset;
double y2 = y - offset;
nodes.insert(to_string(line.b) + "," + to_string(trans(y1)));
nodes.insert(to_string(line.b) + "," + to_string(trans(y2)));
}
else if (d == r) {
nodes.insert(to_string(line.b) + "," + to_string(y));
}
}
else {
double c = -x, d = -y, k = line.k, b = line.b;
double delta = k * k * r * r + r * r - c * c * k * k + 2 * c * d * k +
2 * b * c * k - d * d - 2 * b * d - b * b;
if (delta > 0) {
delta = sqrt(delta);
double x1 = -((delta + k * d + k * b + c) / (k * k + 1));
double y1 = -((k * delta + k * c + d * k * k - b) / (k * k + 1));
double x2 = (delta - k * d - k * b - c) / (k * k + 1);
double y2 = -((-k * delta + k * c + d * k * k - b) / (k * k + 1));
nodes.insert(to_string(trans(x1)) + "," + to_string(trans(y1)));
nodes.insert(to_string(trans(x2)) + "," + to_string(trans(y2)));
}
else if (delta == 0) {
double x1 = (-k * d - k * b - c) / (k * k + 1);
double y1 = (b - d * k * k - k * c) / (k * k + 1);
nodes.insert(to_string(trans(x1)) + "," + to_string(trans(y1)));
}
}
}
分为截距存在和不存在时的情况,同时考虑直线与圆相交,相切的情况,相离就直接略过。
其中有一个trans
函数,由于double
类型精度问题,字符串转换后会出现0.000000
和-0.000000
的情况,实际上都是0,但会被认为是两个点。根据数据限制,误差应该不会超过 \(10^{-10}\) 或者再大几个数量级。这一点在设计上确实存在缺陷。
由于能力不足,总体来看比较暴力,照猫画虎按部就班······
Code Quality Analysis
如上图,整体上没什么警告。有一点需要注意,起初警告说size()
方法得到的是size_t
类型的数据,转换类型可能会造成数据丢失。百度了一下和平台有关,其实问题不是很大,为了好看前面加个强制类型转换,具体可以看这篇博客。