项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2020计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 个人项目作业 |
教学班级 | 005 |
项目地址 | 个人项目作业 |
PSP 2.1表格
PSP 2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 480 | 730 |
Development | 开发 | 520 | 570 |
· Analysis | · 需求分析 (包括学习新技术) | 30 | 60 |
· Design Spec | · 生成设计文档 | 30 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 0 | 0 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 0 | 0 |
· Design | · 具体设计 | 60 | 90 |
· Coding | · 具体编码 | 180 | 240 |
· Code Review | · 代码复审 | 0 | 0 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 180 |
Reporting | 报告 | 70 | 100 |
· Test Report | · 测试报告 | 30 | 60 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 520 | 730 |
解题思路
题目给的直线的参数是两个点,首先选择了直线的公式Ax+By+C=0而不是使用y = kx + b, 可以排除直线垂直于y轴时k不存在的判断条件。进一步决定所需要保存的一些参数。
为了保证不遗漏不重复,任意两个图形之间需要比较一次,这是(O(n^2))的时间复杂度,没有找到有效的消除复杂度的方式,所以优化的核心就是尽可能的剪枝和高效的计算交点。直线与直线之间的交点没有很好的优化方法,联立表达式求解。而直线和圆以及圆和圆之间交点参照这篇文章的算法计算。然后就是剪枝办法,一旦有圆参与就很难剪枝,所以只考虑直线和直线之间的剪枝,因为存在多条直线交于一点的情况,所以计算出交点之后都要与其他已经计算出来的交点比较,排除重复的点。而可以换过来,每新加入一条直线l的时候,遍历所有已知的交点p,如果交点在l上,则所有过交点p的直线都不会与l交于其他的点,所以可以剔除过交点p的所有直线,当遍历完所有已知的交点,剩下的直线与l的交点一定是新的交点,这样可以一定程度上剪枝。当比较多的直线交于一点的时候,会有比较不错的性能的提升,但平行线多的情况下效果会略有下降,平均来说有一定的优化。
设计实现过程
考虑到附加题的圆和直线是两种不同的数据类型,要保存不同的数据,同时进行相交运算的时候需计算方式不同,但是都要进行相交的运算,所以使用了一个抽象父类Geo定义相交运算的接口,圆和直线各自两个不同的Geo子类,保存各自方程需要的参数。而上文说过仅直线参与运算的时候有剪枝的方法,所以还需要一个点类保存交点和过交点的直线。
在此基础上,上文中提到的点圆的相交中用到一些计算的数据可以预先计算出来,在计算相交的时候直接调用,但是这个在仅有直线参与运算的时候是个负优化,所以直线一开始创建的时候不会直接计算参数,当读完输入之后,如果输入中有圆的存在,再对这些参数预先计算并保存。
这样子有一个接口类Geo,有直线和圆通用的将交点添加到交点集的函数,还定义了求交点的接口,判断点是否在图形上的接口;
Geo子类直线Line类,保存自身所需的参数,实现父类的接口,访问保存的参数的函数,还有为了处理与圆的交点进行数据预处理的函数。
Geo子类圆Circle类,同样保存自身所需的参数,实现父类的接口,访问保存参数的函数。
保存点的坐标和过该点直线的Point类,只有输入中没有圆的时候会被使用。
main类,处理输入,创建给类,调用相应的函数进行处理,将结果输出。
具体各类和各函数的功能参照GitHub中项目的ReadMe,本来main应该仅处理输入输出,将其他的功能交给一个容器类,但是由于时间不足主要是懒没有这么做。
因为有上文中的算法基础,而且求交点的步骤不是很复杂,没有流程分析。
函数构建完之后就是单元测试,主要测试各种情况(直线直线相交,直线圆相交,圆圆相交),在这基础上还有交于一点和交于多点,此外还要测试边界条件,比如要求中提到的(-100000,100000)的取值范围。
程序改进
由于在设计的时候考虑到了附加题的圆的情况,并且设计了仅有直线与直线相交的时候的剪枝的情况,所以结构上没有进一步优化。后续使用vector 保存点和自建的point类、pair类效果不佳,改为struct 结构,使用unorder_set管理点集和点重复的情况,因此删除了点类。
代码分析如下图所示
可以看出,其中最占据运算的函数是计算交点的cross()函数,而cross函数如下图所示
函数中最耗时的一步是unordered_set的插入求出来的交点p的一步,这个难以优化。
关键代码展示
具体来说直线的方程式是(l:Ax+By+C=0),而输入为两个不同的点(p_1(x_1, y_2), p_2(x_2,y_2))所以有下列公式得到直线的参数A,B,C:
直线(l:A_1 x + B_1 y + C_1 = 0, l2: A_2 x + B_2 y + C_2 = 0)之间使用(A_1 * B_2 = B_1 * A_2)判断是否平行,不平行则一定有交点
交点的计算公式为
即
double A2 = ((Line*)g)->getA();
double B2 = ((Line*)g)->getB();
double C2 = ((Line*)g)->getC();
if (A * B2 != B * A2) {
double temp_x = (C2 * B - C * B2) / (A * B2 - A2 * B);
double temp_y = (C2 * A - C * A2) / (B * A2 - B2 * A);
struct Point p { temp_x, temp_y };
(*set).insert(p);
}
直线(l:Ax+By+C=0)和圆(C: (x-x_c)^2 + (y-y_c)^2 = r^2)
使用(d^2 = frac{(Ax_c + By_c + C)^2}{A^2 + B^2}=r^2)判断是否有交点其中(sq = A^2 + B^2)
double x = ((Circle*)g)->getX();
double y = ((Circle*)g)->getY();
double d = pow((A * x + B * y + C), 2)/sq;
double s = ((Circle*)g)->getR2() - d;
若有交点, 则垂直于(l)的直线方程为(l2:Bx-Ay+C_2 = 0),带入圆心((x_c, y_c))求出(C2 = A*y_c - B*x_c)
则使用上文的直线与直线相交的方法求出(l2, l1)的交点p。
double C2 = A * y - B * x;
double temp_x = (C2 * B - C * A) / (A * A + B * B);
double temp_y = (C2 * A - C * B) / (A * A + B * B);
若(d^2 = r^2),则(p)为切点,仅有一个交点。
否则可以求出过交点的弦的长度的一半(s = sqrt{r^2-d^2})
直线l的单位向量(overrightarrow{e}, ppmoverrightarrow{e}*s)为直线与圆的两个交点
double l = sqrt(s);
double ex = ((Line*)g)->getEx();
double ey = ((Line*)g)->getEy();
struct Point p1 { temp_x + ex * l, temp_y + ex * l };
(*set).insert(p1);
struct Point p2 { temp_x - ex * l, temp_y - ex * l };
(*set).insert(p1);
圆(C1: (x-x_1)^2 + (y-y_1)^2 = r_1^2),C2:(x-x_2)^2 + (y-y_2)^2 = r_2^2)
使用圆心距(d^2 = (x_1-x_2)^2+(y_1-y_2)^2) 判断两圆之间的关系。
int x2 = ((Circle*)g)->getX();
int y2 = ((Circle*)g)->getY();
int R = ((Circle*)g)->getR();
double a = (double)x2- x;
double b = (double)y2- y;
double r3 = (double)R - r;
double r4 = (double)R + r;
double d2 = a * a + b * b;
double l1 = r3 * r3;
double l2 = r4 * r4;
若(d^2 = (r_1-r_2)^2), 两圆内切,不妨设(r_1<r_2)
则切点(p = c_1+overrightarrow{c_2,c_1}*frac{r_1}{d})
double d = sqrt(d2);
if (r < R) {
struct Point p { x + ((double)x - x2) * r / d, y + ((double)y - y2) * r / d };
(*set).insert(p);
}
else {
struct Point p { x2 + ((double)x2 - x) * R / d, y2 + ((double)y2 - y) * R / d};
(*set).insert(p);
}
若(d^2 = (r_1+r_2)^2), 两圆外切
则切点(p = c_1+overrightarrow{c_1,c_2}*frac{r_1}{d})
double d = sqrt(d2);
struct Point p { x + ((double)x2 - x) * r / d, y + ((double)y2 - y) * r / d };
(*set).insert(p);
若两圆相交则参照上述这篇文章中的第二种方法,
if (d2 > l1&& d2 < l2) {
double d = sqrt(d2);
double s1 = (r2 - ((Circle*)g)->getR2() + d2) / 2 * d;// s1对应文章中的a
double h = sqrt(r2 - s1*s1);
double x0 = x + s1 / d * ((double)x2 - x);
double y0 = y + s1 / d * ((double)y2 - y);
double cx = h / d * ((double)y2 - y);
double cy = h / d * ((double)x2 - x);
struct Point p1 { x0 - cx, y0 - cy };
(*set).insert(p1);
struct Point p2 { x0 + cx, y0 + cy };
(*set).insert(p2);
}