软工个人项目作业
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2020春季计算机学院软件工程(罗杰 任建) |
这个作业的要求在哪里 | 个人项目作业 |
我在这个课程的目标是 | 学习软件工程相关知识,培养自己独立和团队开发能力 |
这个作业在哪个具体方面帮助我实现目标 | 学会使用Visual Studio进行代码的效能分析,熟悉个人软件开发流程 |
作业正文...... | 见下文 |
其他参考 | 见文中“参考资料”的链接 |
教学班级 | 005 |
项目地址 | https://github.com/yangjiuchun123/SE_personal_homework.git |
PSP2.1表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 20 | 20 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 40 | 80 |
· Design Spec | · 生成设计文档 | 20 | 20 |
· Design Review | · 设计复审 (和同事审核设计文档) | 0 | 0 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 5 |
· Design | · 具体设计 | 30 | 30 |
· Coding | · 具体编码 | 100 | 80 |
· Code Review | · 代码复审 | 30 | 30 |
· Test | · 测试(自我测试,修改代码,提交修改) | 100 | 200 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 60 | 100 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 20 |
合计 | 440 | 595 |
表格中,测试所用的时间比预期的高了很多,主要原因是这其中还包含了写单元测试以及自动生成测试样例程序的时间,这些是原来没有预想到的。此外需求分析的时间也比预估多了一些,这是因为网上查阅的资料中,那些复杂的数学推导要去验证正确性花了比计划要多的时间。
解题思路
本题最简单的思路就是暴力枚举法:每次选取两条直线(或一条直线和一个圆,或两个圆),首先判断他们是否相交,如果相交就求出它们的交点坐标并存入一个set中,遍历完所有的图形对之后,set中的元素个数即为交点个数。以下分情况讨论交点的坐标。
(1)两条直线的交点
两条直线的交点坐标比较容易求,可以通过简单的数学推导得到:
首先由于直线是以两点式$(x1,y1),(x2,y2)$的形式给出的,我们要先把它转化为一般式,方便计算,即:
$L: ax+by+c=0$,其中$a=y1-y2,b=x2-x1,c=x1y2-x2y1。$
之后可以求出两直线$L1: a1x+b1y+c1=0$和$L2: a2x+b2y+c2=0$交点的坐标为:
$x = (b1c2 - b2c1) / d, y = (a2c1 - a1c2) / d$,其中$d=a1b2-a2b1$,$d$为$0$时意味着两直线平行,没有交点。
参考资料:求两直线交点坐标
(2)两个圆的交点
这个数学推导非常复杂麻烦,直接解方程基本不可能解得出来,上网查阅资料后发现有使用参数方程的解法,但是其中还需要根据结果的三角函数值进行分类讨论以确定正负号的问题,这会使程序测试起来比较困难,因此我没有采用。后面看到有使用Matlab来解方程,直接求出解析解的方法,我认为这种方法虽然简单粗暴,但不需分类讨论可以直接得到通解,是个比较好的方法,故我选择了使用Matlab来进行方程的求解,解出的式子也非常长,就不放在这里了。需要注意的是,当两圆没有交点的时候,Matlab求出来的解是没有意义的,所以在直接套公式之前需要判断一下。
求解的Matlab代码如下:
clear all
syms a1 b1 R1 a2 b2 R2 x y;
[x,y]=solve((x-a1)*(x-a1)+(y-b1)*(y-b1)-R1*R1,(x-a2)*(x-a2)+(y-b2)*(y-b2)-R2*R2);
%simplify(x)
%simplify(y)
参考资料:参数方程的解法
(3)一条直线和一个圆的交点
这个数学推导也比较复杂,在发现Matlab这个好用的工具之后,这次我直接使用了Matlab联立两个方程来求出解析解,解出的式子也非常长,同样不放在这里了。需要注意的是直线与圆的交点数量可能有三种情况:没有交点、有一个交点、有两个交点。而Matlab解出来的x与y分别有两个,如果直接使用x与y的公式可能会出现根号下为负的情况(因为原方程在实数域内不一定有解),故我们应该首先判断直线与圆是否有交点,如果有交点才可以套用公式。此外,Matlab解出的方程在直线的斜率为0时会出现除数为0的情况,因此需要特判一下直线的斜率是否为0。
求解的Matlab代码如下:
clear all
syms a1 b1 c1 a b r x y;
[x,y]=solve((x-a)*(x-a)+(y-b)*(y-b)-r*r, a1*x+b1*y+c1);
%simplify(x)
%simplify(y)
实现过程
设计上我最初的想法是3个类:一个shape类,一个line类,一个circle类。line类和circle类均是由shape类继承而来。顾名思义,line类存储直线的相关属性,而circle类存储圆的相关属性。shape类里记录了该图形的类型,即是L(line)还是C(circle),line类里存储了直线的两点坐标,circle类里存了圆心坐标和半径,两个类均有一个showStatus函数来输出它们的所有属性,以便调试。
不过后来,我在尝试单元测试的时候发现,单元测试是针对某一个类进行的测试,但我的主要算法并不在某一个类里,而直接写成了函数,这样无法进行单元测试。因此后来我将求解交点个数的过程重新整合为一个Solve类,这样方便单元测试的进行。单元测试就主要测试这个Solve类的函数,包括getIntersect(求解总的交点个数)、LLintersect(求解两条直线的交点坐标)、CCintersect(求解两个圆的交点坐标)、CLintersect(求解圆和直线的交点坐标)。
总体来看函数虽然公式很复杂,但是逻辑上并不复杂,就不需要画出流程图了。
性能改进
整个算法使用的是暴力枚举法,因此时间复杂度为$O(n^2)$。我也曾思考过有没有什么更好的方法,并上网查阅了有关的资料,但很多题目都有个前提条件就是“无三线共点”,例如C++ 计算直线的交点数这道题。这和我们的题目要求是有差距的,如果无三线共点可以使用动态规划解,不需要考虑交点的坐标;而如果没有这个前提,那么还需要考虑交点重合的问题,因而必须计算出坐标,用动态规划解就比较困难了。
因此在性能改进这部分我只能做一些局部计算上的性能改进,例如减少一些重复的计算过程等,重复计算的情况在用Matlab求出的交点坐标公式中还是比较常见的,因此这部分确实有一些改进的空间。
性能改进这部分花了我大概一个多小时的时间,主要时间都在看网上的类似题目的代码并思考能不能应用到我们的题目上,可惜并没有什么好的思路。最终采用的减少重复计算的改进花了半小时左右,因为公式很长很复杂,需要在整理的时候多加细心一些。
性能分析图如下:
可以看到,LLintersect这个函数消耗的CPU比较多,这可能与测试数据中直线比较多有关,但这也是我或许可以提升性能的一个点。
代码说明
1、Solve类的getIntersect函数的代码:
void Solve::getIntersect(Shape* s1, Shape* s2, set<pair<double, double>>* interSet) { //计算两条图形的交点
char t1, t2;
t1 = s1->type;
t2 = s2->type;
if (t1 == 'L' && t2 == 'L') { //求两直线的交点
LLintersect((Line*)s1, (Line*)s2, interSet);
}
else if (t1 == 'C' && t2 == 'C') { //求两圆的交点
CCintersect((Circle*)s1, (Circle*)s2, interSet);
}
else { //求直线与圆的交点
if (s1->type == 'L' && s2->type == 'C') {
CLintersect((Circle*)s2, (Line*)s1, interSet);
}
else if (s1->type == 'C' && s2->type == 'L') {
CLintersect((Circle*)s1, (Line*)s2, interSet);
}
else {
cout << "something goes wrong!" << endl;
}
}
}
这个函数主要是判断现在取出的两个图形分别是什么,并分情况将它们交给其他的分类处理函数处理。由于我采用了一个shape类的指针数组来存储所有的图形,并依次取出一对计算交点,因此这里取出的一对图形都是shape类的,但下面的分类处理函数需要的参数都是circle或者line,所以需要将shape类进行强制类型转换才能作为参数传给下面的函数。
2、Solve类的CCintersect函数的代码:
void Solve::CCintersect(Circle* c1, Circle* c2, set<pair<double, double>>* interSet) {
double a1 = c1->x;
double b1 = c1->y;
double r1 = c1->r;
double a2 = c2->x;
double b2 = c2->y;
double r2 = c2->r;
if ((pow((a1 - a2), 2) + pow((b1 - b2), 2)) > pow(r1 + r2, 2)) {
return; //两圆不相交
}
else {
double value1 = a1 * a1 - 2 * a1 * a2 + a2 * a2 + b1 * b1 - 2 * b1 * b2 + b2 * b2;
double value2 = -r1 * r1 * a1 + r1 * r1 * a2 + r2 * r2 * a1 - r2 * r2 * a2 + a1 * a1 * a1 - a1 * a1 * a2 - a1 * a2 * a2 + a1 * b1 * b1 - 2 * a1 * b1 * b2 + a1 * b2 * b2 + a2 * a2 * a2 + a2 * b1 * b1 - 2 * a2 * b1 * b2 + a2 * b2 * b2;
double value3 = -r1 * r1 * b1 + r1 * r1 * b2 + r2 * r2 * b1 - r2 * r2 * b2 + a1 * a1 * b1 + a1 * a1 * b2 - 2 * a1 * a2 * b1 - 2 * a1 * a2 * b2 + a2 * a2 * b1 + a2 * a2 * b2 + b1 * b1 * b1 - b1 * b1 * b2 - b1 * b2 * b2 + b2 * b2 * b2;
double sigma = sqrt((r1 * r1 + 2 * r1 * r2 + r2 * r2 - a1 * a1 + 2 * a1 * a2 - a2 * a2 - b1 * b1 + 2 * b1 * b2 - b2 * b2) * (-r1 * r1 + 2 * r1 * r2 - r2 * r2 + value1));
double p1_x = (value2 - sigma * b1 + sigma * b2) / (2 * value1);
double p2_x = (value2 + sigma * b1 - sigma * b2) / (2 * value1);
double p1_y = (value3 + sigma * a1 - sigma * a2) / (2 * value1);
double p2_y = (value3 - sigma * a1 + sigma * a2) / (2 * value1);
(*interSet).insert(pair<double, double>(p1_x, p1_y));
(*interSet).insert(pair<double, double>(p2_x, p2_y));
return;
}
}
这里首先需要判断两圆是否相交,如果不相交则直接return即可。后面则是通过Matlab解出的公式来计算两个交点的坐标,注意到在公式中很多的项的组合是重复的,因此可以将这些重复项先算出来,以减少计算量。算出两个交点的坐标后,把它们作为一个pair加入到set当中,这样即使是两个相同的交点也可以自动去重。