个人项目作业

项目

内容

这个作业属于哪个课程

2020春季计算机学院软件工程(罗杰 任健)

这个作业的要求在哪里

个人项目作业

我在这个课程的目标是

通过这门课锻炼软件开发能力和经验,强化与他人合作的能力

这个作业在哪个具体方面帮助我实现目标

进一步应用所学的软件工程知识,构建项目

项目Github地址

程序的各个模块的开发上耗费的时间估计

PSP2.1

Personal Software Process Stages

预估耗时(分钟)

实际耗时(分钟)

Planning

计划

10

20

· Estimate

· 估计这个任务需要多少时间

10

20

Development

开发

230

325

· Analysis

· 需求分析 (包括学习新技术)

10

10

· Design Spec

· 生成设计文档

15

15

· Design Review

· 设计复审 (和同事审核设计文档)

0

0

· Coding Standard

· 代码规范 (为目前的开发制定合适的规范)

10

10

· Design

· 具体设计

15

20

· Coding

· 具体编码

90

120

· Test

· 测试(自我测试,修改代码,提交修改

90

180

Reporting

报告

30

40

· Test Report

· 测试报告

10

10

· Size Measurement

· 计算工作量

10

10

· Postmortem & Process Improvement Plan

· 事后总结, 并提出过程改进计划

10

20

合计

240

345

解题思路描述

这个问题的常规解法还是比较容易想的,实质上就是求多条直线的交点个数,可以将其抽象为数学问题,设直线方程为:

\[Ax + By + C = 0 \]

首先通过\(x1, x2, y1, y2\)求解\(A, B, C\):

\[A = y2 - y1\\ B = x1 - x2\\ C = x2*y1-x1*y2 \]

再紧接着求\(b\),那么两条直线的交点就是:

\[x=\frac {b_1c_2-b_2c_1}{a_1b_2-a_2b_1},y=\frac {a_2c_1-a_1c_2}{a_1b_2-a_2b_1} \]

计算出结果之后将其放入set当中(自动去重),最后统计个数即可。

对于附加任务,本质上没有区别,也就是求交点,也就是解直线和圆以及圆和圆之间联立的方程,对于k = 无穷的情况令行考虑:

\[\begin{gather} y = kx + b\\ (x - a)^2 + (y-b)^2 = r^2 \end{gather} \]

以及:

\[(x - a_1)^2 + (y-b_1)^2 = r_1^2\\ (x - a_2)^2 + (y-b_2)^2 = r_2^2 \]

联立求解即可,方法类似,但是这么做的时间复杂度为\(O(n^2)\),效率注定是低下的,而且还包含了相当耗费时间的浮点运算,对于规模较大的数据就会无能为力,查阅了许多资料但暂时还没有找到改进的办法。

设计实现过程

代码上我设计了三个类,即circle、line、dot,分别表示圆、直线、点。

主函数:每次输入一个图形序列,根据序列选择图形种类,与其他所有图形进行相交运算,将所得的结果存入交点集合,计算细节由其他函数完成。

/*输入*/
void input(int num);
/*求交点*/
struct dot calculate(line A, line B);
/*求二元一次方程*/
vector<double> level2Equation(vector<double> simple);
/*求相交直线*/
vector<double> getEquationForLC(line x, circle y);
/*求圆与直线的交点*/
void getLCcrossDot(line A, circle B);
/*求相交弦所在的直线*/
line* get2CircleLine(circle a, circle b);
/*求k=无穷的直线的交点*/
void yParelLine(line A, circle B);

line类:表示直线\(Ax+By+C=0\)。

circle类:表示圆\((x-a)^2+(y-b)^2=r^2\)。

dot类:表示坐标(x, y)。

单元测试方面,针对每一个函数构造了多个测试用例,例如对于求二元一次方程:

TEST_METHOD(level2EquationTest) {
		std::vector<double> test = { 1, -4, 3 };
		std::vector<double> solution = level2Equation(test);
		Assert::AreEqual(solution[0], 3.0);
		Assert::AreEqual(solution[1], 1.0);
}

构造了多个方程,根据delta>0, =0, <0的情况的方程来检查求根公式的应用是否正确。

再例如求两个圆相交弦所在的直线:

TEST_METHOD(getCircleLineTest){
		circle A(0, 0, 1);
		circle B(1, 1, 1);
		line* test = get2CircleLine(A, B);
		Assert::IsNotNull(test);
		Assert::AreEqual(test->k, -1.0);
		Assert::AreEqual(test->b, 1.0);
	}

构造了两个圆,首先检查两元关系是否是相交,再检查其相交弦所在的直线是否正确。

编写的单元测试均包含了所有的情况(即所有的分支)。

改进程序性能上所花费的时间

我在许多方面尝试过提高程序的效率,但是仅仅也只是常数方面的优化(8-2原则),本质上还是算法的效率太差(\(O(n^2)\)),如果要想改进程序性能,不如做那耗费80%时间模块的优化,因此还是将注意力集中在算法的优化上更好,但是现在还是没有研究出一个时间复杂度更低的算法,现在想到的方法有改进求交点算法或者改进增加交点时检查是否有重复的效率。

下面是对于4000个图形的时间性能测试:

grafana两条线相加_ci

从性能分析表中看,算法的大部分时间耗费在了求解圆与直线的交点上,这部分属于算法的问题,暂时没有可以改进的地方。

程序消耗最大的函数其实是set的insert方法,因为点比较多的缘故,维护这棵树的代价也会比较高,如果暂时想不出改进求交点算法的话,考虑降低维护交点树的成本也是一个选择。

代码说明

关键代码也就是那几个求解交点的数学运算,在基础作业方面,关键代码即求交点:

struct dot calculate(line A, line B) {
	struct dot result;
	if (A.a * B.b == A.b * B.a) {
		return result;
	}
	result.x = (A.b * B.c - A.c * B.b) / (A.a * B.b - B.a * A.b);
	result.y = (B.a * A.c - B.c * A.a) / (A.a * B.b - A.b * B.a);
	crossDot.insert(result);
	return result;
}

若平行则忽略,否则计算交点插入。

附加作业方面:如求两个圆相交弦所在的直线:

line* get2CircleLine(circle a, circle b) {
	double distance = pow(a.x - b.x, 2) + pow(a.y - b.y, 2);
	/* 相离 */
	if (distance > pow(a.r + b.r, 2)) {
		return NULL;
	}
	/* 内含 */
	else if (distance < pow(a.r - b.r, 2)) {
		return NULL;
	}
	double _a = 2 * (b.x - a.x);
	double _b = 2 * (b.y - a.y);
	double _c = a.x * a.x - b.x * b.x + a.y * a.y - b.y * b.y - a.r * a.r + b.r * b.r;
	return new line(_a, _b, _c);
}

主要思想即联立方程组消二次项系数、得到相交弦所在直线的斜率和y轴交点,再将任一圆和此直线方程联立,求直线与圆的交点即可:

void getLCcrossDot(line A, circle B) {
	vector<double> simple = getEquationForLC(A, B);
	vector<double> solution = level2Equation(simple);
	double k = -A.a / A.b;
	double b = -A.c / A.b;
	if (solution.size() == 1) {
		crossDot.insert(struct dot(solution[0], k * solution[0] + b));
	}
	else if (solution.size() == 2) {
		crossDot.insert(struct dot(solution[0], k * solution[0] + b));
		crossDot.insert(struct dot(solution[1], k * solution[1] + b));
	}
}

其他

本项目已经消除Code Quality Analysis 中的所有警告

grafana两条线相加_ci_02

单元测试已测试了可返回函数的所有情况(分支):

grafana两条线相加_ci_03