软件工程个人项目作业——平面交点个数

  1. 在文章开头给出教学班级和可克隆的 Github 项目地址。(1')
项目 内容
这个作业属于 2020春季计算机学院软件工程(罗杰 任健)
这个作业的要求是 个人项目作业
我的教学班级 006
本项目的GitHub地址 IntersectProj
我对这项作业的目标是 提高个人程序开发素质,写出高性能程序

PSP

  1. 在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。(0.5')
  2. 在你实现完程序之后,在下述 PSP 表格记录下你在程序的各个模块上实际花费的时间。(0.5')
  • PSP
PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
- Estimate - 估计这个任务需要多少时间 10 10
Development 开发
- Analysis - 需求分析 (包括学习新技术) 75 120
- Design Spec - 生成设计文档 10 15
- Design Review - 设计复审 (和同事审核设计文档) 5 10
- Coding Standard - 代码规范 (为目前的开发制定合适的规范) 5 2
- Design - 具体设计 30 30
- Coding - 具体编码 60 180
- Code Review - 代码复审 15 60
- Test - 测试(自我测试,修改代码,提交修改) 45 240
Reporting 报告
- Test Report - 测试报告 10 10
- Size Measurement - 计算工作量 5 5
- Postmortem & Process Improvement Plan - 事后总结, 并提出过程改进计划 10 15
合计 280 697
  • 反思
    • 实际编码时间是计划的3倍左右
    • 主要问题在于第一次用C++写面向对象的程序,不了解VS平台的各种工具和提示,对问题的规模没有正确的判断。
    • 这也就导致后面最初构思问题非常大,之后反复改思路,修改程序
    • 浪费了很多时间,最终程序的效率和正确性都难以保障。

构思

解题思路

  1. 解题思路描述。即刚开始拿到题目后,如何思考,如何找资料的过程。(3')
  • 解题思路分为两部分
    1. 具体计算方法
    2. 计算优化构思

具体计算方法

  1. 求直线方程
  • 两点求直线,回顾高中数学,参考博文之后,采用直线一般式方程\(Ax + By + C = 0\),并在\(O(1)\)时间复杂度内计算出直线方程
  1. 计算交点
  • 计算交点也是有公式,可以在\(O(1)\)时间复杂度内得出
  • 但计算新增直线和前序直线的交点,是对时间复杂度影响很大的步骤
  • 很容易想到的一个暴力解法:
    • 每新增直线n,新增交点数是\(n-1-N(平行线数)-N(过交点线数)\),同时维护平行斜率集合和已有交点集合
    • 该算法时间复杂度为\(O(n^2)\)
  1. 关于交点精确度问题
  • 尝试自定义分数类来表示

代码设计

  1. 设计实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?单元测试是怎么设计的?(4')
  • 基本元素(class)
    • 点 Point
      • 点构造函数 Point(x,y)
      • 点与点关系维护函数(equal)
      • 交叉点所属直线维护函数(add, contains, size)
    • 线 Line
      • 直线两点构造函数 Line(point1, point2)
      • 直线间关系判断函数(parallel, intersect, equal)
    • 直线集合 lineSet
    • 交点集合 interSet
    • 分数计算类 Radio
      • 加减乘除、分子分母最小化
  • 每次加入新直线的流程图
graph TD A[构造新直线] --> B[判断交叉,并维护交叉集合] B --> C[得到交叉线集合delSet] C --> D[遍历除delSet之外的前序输入的直线集合,并维护新的交叉集合]

性能改进

  1. 记录在改进程序性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由 VS 2019 的性能分析工具自动生成),并展示你程序中消耗最大的函数。(3')
  • 在编写程序时,我主要面对了两大问题,这两大问题也严重影响了整体性能

    • 不熟悉C++代码
    • 构思时对问题没有深入准确的理解
  • 尤其是第二个问题,导致了第一次运行时效能严重低下,最终需要多次代码重构,

  1. 最初设计Radio分数类,其中一些求取最大公倍数的函数导致程序性能异常低下,以至于1min也只能跑500+条直线
  • 其中分数类Radio占用了接近50%的CPU使用率,而求取最大公约数函数则是其中的大头
  • 之后参考位运算——高效的最大公约数算法来优化,但是最后效果平平,放弃使用Radio类来提高精度而采用float

2Radio效能分析

  1. 程序最初设计采用遍历,加之除去平行线、交点剪枝,以提高程序性能,但是最终实现时候,最初的设计delSet设计导致CPU占用率极高
  • 遍历交叉点,而且在交叉点的直线集合中查找,导致整个程序效能也只能达到1min600+条直线,所以删去这两种优化
  • 反思:并不是这两种方法不能提高性能,而是我在构思和编写中没有采用更好的数据结构和算法
  1. 最终的性能分析图
  • 程序中消耗最大的函数是main函数中的向交叉点集合中插入交叉点的插入操作,这让我想我试用unordered_set来进一步优化,只可惜DDL迫在眉睫;消耗次之的函数是main函数中的前序直线组遍历操作,这是我们在最初的分析中遇见到的

2最终效能分析

代码综述

  1. 代码说明。展示出项目关键代码,并解释思路与注释说明。(3')

代码说明

  • 采用Top-down的思路来阐述我的代码
graph TD A[直线与交点集合操作] --> B[直线,交点类] B --> C[浮点数确保精度] B --> D[直线相交平行计算]
  • main.cpp中的直线与交点集合操作
    • 前序直线集合顺序遍历和尾部添加,可以采用List这种链表型数据结构
    • 交点集合需要避免重复,而且需要较高的查找速度,故采用以红黑树为数据结构的set(也可以尝试以hash为索引结构的unordered_set)
// 直线和交点集合
vector<Line> lineList;
set<Point> interSet;

// 遍历
for (i = 0; i < n; i++) {
		input >> c;
		if (c == 'L') {
			input >> x1 >> y1 >> x2 >> y2;
			Line l(x1, y1, x2, y2);
			
			for (auto iter = lineList.begin(); iter != lineList.end(); iter++) {
				Line lit = (Line)* iter;
				if (lit.isParallel(l)) {	
					continue;
				}
				// 交点计算和新增
				Point pInter = l.getIntersect(lit);
				interSet.insert(pInter);
			}
			lineList.push_back(l);
		}
	}
  • Graph.h & cpp中的点和线类
    • 其中用于set排序点operator< & operator==重载,都是比较重要的地方
class Point
{
public:
	float x;
	float y;

	Point(float xNew, float yNew);
	bool equal(Point p);
	float getX();
	float getY();
	// 重载
	bool operator<(const Point& p) const {
		if (!EQFLOAT(x, p.x))
			return x < p.x;
		else
			return y < p.y;
	}
	bool operator==(const Point& p) const {
		return EQFLOAT(x, p.x) && EQFLOAT(y, p.y);
	}
	
private:
};
  • 直线的参数、斜率、是否含有某点、取得交点,都很重要
class Line
{
public:

	Line(int x1, int y1, int x2, int y2);
	float getA();
	float getB();
	float getC();
	float getslope();
	bool isParallel(Line l);
	bool containsPoint(Point p);
	Point getIntersect(Line l);	
	bool equal(Line l);

private:
	// line: Ax + By + C = 0;
	float A;
  float B;
	float C;
	float slope;
};

// 计算直线参数极其斜率
Line::Line(int x1, int y1, int x2, int y2) {
	A = (float) y2 - y1;
	B = (float) x1 - x2;
	C = (float) x2 * y1 - x1 * y2;
	if (x1 - x2 == 0) {
		slope = FLT_MAX;
	}
	else {
		slope = (float)(y1 - y2) / (x1 - x2);
	}
}
  • 在判断两交点是否重合的问题上,需要引入浮点数的精度计算
#define EQS (1e-8)
#define EQFLOAT(a,b) (fabs((a) - (b)) < EQS)

bool Point::operator==(const Point& p) const {
		return EQFLOAT(x, p.x) && EQFLOAT(y, p.y);
	}
  • 最后取得直线的交点,判断直线是否平行,也是关键
Point Line::getIntersect(Line l) {
	float a2 = l.getA();
	float b2 = l.getB();
	float c2 = l.getC();
	float x = (B * c2 - C * b2) / (A * b2 - B * a2);
	float y = (C * a2 - A * c2) / (A * b2 - B * a2);
	Point p(x, y);
	return p;
}

bool Line::containsPoint(Point p) {
	float res = A * p.getX() + B * p.getY() + C;
	return EQFLOAT(res, 0);
}

测试与调试

  • 单元测试
    • 覆盖性的测试了Point和Line两个类的成员函数的功能
		TEST_METHOD(TestMethodPoint1) {
			Point p(0.5, 3);
			Assert::AreEqual(p.getX()== 0.5, true);
			Assert::AreEqual(p.getY()==3, true);
			Point m(0.5, -3);
			Assert::AreNotEqual(p.equal(m), true);
		}

		TEST_METHOD(TestMethodLine1) {
			Line l1(0, 0, 1, 1);
			Line l2(0, 2, 1, 0);
			Line l3(0, -45, 45, 0);
			Line lr(1, 0, 5, 0);
			Line bt(1, 1, 1, 10);
			Assert::AreEqual(l2.getA()== -2, true);
			Assert::AreEqual(l2.getB()== -1, true);
			Assert::AreEqual(l2.getC()==2, true);
			Assert::AreEqual(l2.getslope()==-2, true);
			// parallel
			Assert::AreEqual(l1.isParallel(l3), true);
			Assert::AreEqual(lr.isParallel(bt), false);
			// containsPoint
			Point e(0.5,1);
			Point base(0,0);
			Assert::AreEqual(l2.containsPoint(e), true);
			Assert::AreNotEqual(l1.containsPoint(e), true);
			Assert::AreEqual(l1.containsPoint(base), true);
			// get intersect
			Point inter12((float)2/3,(float)2/3);
			Assert::AreEqual(l1.getIntersect(l2).equal(inter12), true);
			Point inter3lr(45, 0);
			Point inter3tb(1, -44);
			Assert::AreEqual(l3.getIntersect(lr).equal(inter3lr), true);
			// equal
			Assert::AreEqual(l1.equal(l2), false);
		}
  • 调试辅助工具
    • GeoGebra是一项可视化界面精良的轻量级数学绘图工具,使用它也辅助我发现了程序的一个bug

2图形界面debug

  • 覆盖性压力测试
    • 借鉴了同学随机生成点阵的代码,生成了大量的几千数据量的黑盒测试集来辅助测试
    • 但是黑盒测试问题在于数据量过大,一旦对拍出现不一致也很难找到问题。

编程反思

  • 这次作业我差不多断断续续写了两天,由于C++和平台不熟悉,以及寒假项目也没有太多软件开发任务,所以这一次作业从完成结果和过程上看,都不尽人意。后面的软件工程开发还有很重的担子。

  • 下面记录一些问题和需要改进的地方

  • 问题

    • 设计文档需要细化到哪个细粒度?哪些工作是需要代码完成的?
    • 写代码时改设计文档是一件可以避免的事吗?如何避免
  • 改进

    • 由于编程能力的问题,我之后需要在学习各种方法的同时,留更多的时间给程序编写,以保证作业质量,比如这次作业,假如能提前一天时间开始写,应该会大有长进
    • 需要更熟悉C++这门语言,以及VS IDE,保证之后多人开发的效率
posted @ 2020-03-10 18:53  yzy11235  阅读(243)  评论(2编辑  收藏  举报