BUAA软工__个人项目作业

项目 内容
这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) (北京航空航天大学 - 计算机学院)
这个作业的要求在哪里 个人项目作业
我的教学班级 005
这个项目的GitHub地址 https://github.com/LastWhisper1/IntersectionCounter

PSP项目表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 10 5
Development 开发
· Analysis · 需求分析 (包括学习新技术) 240 180
· Design Spec · 生成设计文档 90 60
· Design Review · 设计复审 (和同事审核设计文档) 30 30
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10 10
· Design · 具体设计 60 90
· Coding · 具体编码 90 90
· Code Review · 代码复审 20 20
· Test · 测试(自我测试,修改代码,提交修改) 90 180
Reporting 报告
· Test Report · 测试报告 30 30
· Size Measurement · 计算工作量 30 30
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30 30
合计 730 755

从实际结果来看,整个项目的实现时间还是太长了。我码代码的能力果然还是令人捉急,虽然前期有过很复杂的想法,但在具体编码时并没有得到实现,对最后的程序性能也不太满意。虽然不熟悉C++可以作为一部分原因,但总的来说在效率上还是有很大的提升空间,今后还是要加强自己的编码能力,避免有想法而实现不了的情况。

解题思路描述

为了避免各种误差对结果造成的影响,解题中避免了使用浮点数,具体的实现方式上,使用了以下三个类:

1. 直线类Line

直线采用最万能的标准式

\[Ax+By+C=0 \]

来保存,通过给定的两点计算出三个参数即可,并且无需化简。

这个类中还实现了检验两条直线是否平行的函数,由于不考虑重合,只需对两条直线

\[\left\{\begin{matrix} & A_{1}x + B_{1}y + C_{1}=0\\ & A_{2}x + B_{2}y + C_{2}=0 \end{matrix}\right. \]

验证是否成立

\[A_{1}B_{2}=A_{2}B_{1} \]

即可。

2. 交点类Intersection

题目要求已经确保直线不会重合,通过函数验证两条直线不平行后,方程

\[\left\{\begin{matrix} & A_{1}x + B_{1}y + C_{1}=0\\ & A_{2}x + B_{2}y + C_{2}=0 \end{matrix}\right. \]

的唯一解是

\[\left\{\begin{matrix} x = \frac{C{_{1}}B{_{2}}-C_{2}B_{1}}{B{_{1}}A{_{2}}-B_{2}A_{1}}\\ y = \frac{A{_{1}}C{_{2}}-A_{2}C_{1}}{B{_{1}}A{_{2}}-B_{2}A_{1}} \end{matrix}\right. \]

在保存时,分别保存\(x, y\)的分子和分母(都是整数),这样就避免了浮点数导致的精度问题。如果接下去考虑把圆加进来的话,可以再额外保存根式下的分子和分母。但程序中没有实现圆相关的部分,故不再赘述。

3. 交点数统计类Counter

每读入一条直线便与每一条现有直线比较,若两条直线不平行,则计算交点值并放入set容器中,最后将新读入的直线放入vector容器中。这样做的话,如果使用unordered_set,在平均情况下的时间复杂度为\(O(n^{2})\),若使用set则为\(O(n^{2}logn)\)

在set与unordered_set的选择中,发现程序在输入规模 \(N = 10000\) 时,使用set会导致超过60秒时间限制,但使用unorder_set会导致bad_alloc异常。经过权衡,既然想不到更好的方法提升性能,不如使用set尽可能保证准确性,也能避免使用unorder_set后因hash函数导致的bug。

4. 优化上的一些没实现的想法

程序中,存储和计算交点是必不可少的工作量。对每个交点,如果在计算中,发现直线通过某个交点,那么该直线和通过这个交点的所有直线,都不可能再有其他交点,这样就可以省去和一些直线计算交点的步骤。考虑到性能测试中交点个数\(h\)远小于\(N(N-1)/2\),暗示多线共点的情况较多,那么这种方法能起到比较大的优化作用。

针对平行线的优化可能收效不大,举个例子,若一条直线与一组\(m\)条平行线中的某一条相交,可以知道该直线与该组平行线共产生了\(m\)个交点,但由于存在多线共点的情况,仍需要计算每个交点的位置。

设计实现过程

如上一段所说,我实现了Line,Intersection,Counter三个类,并采用了上面所说的容器。在命令行参数的识别上,采用比较普通的while循环来实现参数的读取和处理。下面主要讲一下使用的测试方法与测试样例的构建。

在构建的单元测试样例中,我做了基本的功能测试和边界测试。在基本的功能测试中,我尽可能测试了每个函数的功能是否正确,包括直线标准式是否正确、交点计算是否正确(也就是Line和Intersection的构造函数),同时构建了几个比较基础的样例测试。在边界测试中,考虑了诸如边界点确定直线、两条直线的交点非常接近的情况进行样例构建,测试结果并没有问题。下面这张图描述了其中一个测试样例的构建:

在性能方面的压力测试中,我另写了一个cpp文件(代码不在git仓库中),使用随机生成的方法构建大量样例。虽然这样生成的直线并不能保证不重合(实际上概率非常小),但在我自己的程序逻辑中,两条直线重合被视为平行,不会引发崩溃性的bug,故在性能测试中可以接受。实际上,随机生成的直线几乎不存在平行或重合的情况,得出的交点数基本都是\(N(N-1)/2\)

程序性能改进

程序写好以后,实际上并没有做大的框架上的改动,但在细节处理上还是改过很多地方。例如,判断两条直线是否平行的函数和Counter类中插入新直线的函数,由于函数体较为简短,故改成了inline函数提高性能。另外,程序中还尝试使用double代替之前介绍的分数表示方法来存储交点,实际显示性能提升不是那么明显。如下是在使用set容器存储交点集合,\(N = 5000\)时性能测试的截图:

从截图中可以看出,程序运行时间中相当一部分都在set的RB树构建上(当然之前测试的时候也对其他时间占比较大的地方进行了优化)。

若把set替换成unordered_set的话,性能会有比较明显的提升(经测试,在相同\(N\)值下,平均可减少\(1/3\)执行时间),但由于没办法很好解决掉内存分配异常的问题,加上hash函数可能存在错误,故还是采用set容器尽可能保证程序正确性。

关键代码说明

首先展示交点的构造函数,这里的A1, B2等使用了宏定义,对应了两条直线标准式的相应参数,具体含义之前的部分已经提及:

Intersection::Intersection(Line* line1, Line* line2) {
	int xnume = C1 * B2 - C2 * B1;
	int xdeno = B1 * A2 - B2 * A1;
	int ynume = A1 * C2 - A2 * C1;
	int ydeno = xdeno;
}

代码中的xnume对应交点x坐标的分子,xdeno对应x的分母,y坐标依此类推。通过分子分母分别存储的方法,避免了浮点数可能带来的精度问题。

下面是整个程序的核心函数CountIntersections:

int Counter::CountIntersections() {
	for (size_t i = 0; i < lineSet->size(); i++) {
		Line* line1 = lineSet->at(i);
		for (size_t j = 0; j < i; j++) {
			Line* line2 = lineSet->at(j);
			if (!line1->isParallel(line2)) {
				Intersection intsec(line1, line2);
				intersectionSet->insert(intsec);
			}
		}
	}
	return intersectionSet->size();
}

代码的逻辑在上文中已经介绍过了,这段函数中的isParallel()为平行线的判定函数,并写成了内联函数以提高性能。若想要继续提高这个函数的性能,除了将set改成unordered_set之外,只能从循环条件出发,略去一些不必要的交点计算。

Code Quality Analysis截图

还请老师助教提出批评意见。

3/10追记:在和结对搭档对拍程序的时候,发现了比较严重的bug,初步判断是Intersection类中运算符重载错误引起的,但短时间内难以找出消除bug的方法。在之前和搭档对double精度的讨论中,同伴的观点是double精度足够完成实验需求,所以在最后一次提交时,利用程序中预留的宏定义开关将Intersection类改为double实现,直接存储交点x, y坐标的double值(在博客代码的基础上做除法即可得到)。

posted @ 2020-03-08 14:40  P.R.E.T.T.Y  阅读(286)  评论(2编辑  收藏  举报