个人项目作业

个人项目作业

项目 内容
这个作业属于哪个课程 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类型的数据,转换类型可能会造成数据丢失。百度了一下和平台有关,其实问题不是很大,为了好看前面加个强制类型转换,具体可以看这篇博客

posted @ 2020-03-09 21:37  Geraint23  阅读(169)  评论(2编辑  收藏  举报