本次做业是北航计算机学院软件工程课程的我的项目做业,我的开发能力对于软件开发团队是相当重要的,本项目旨在经过一个求几何图形的交点的需求来使学生学会我的开发的经常使用技巧,如PSP方法,需求分析,设计文档,编码实现,测试,性能评价等等。java
项目 | 内容 |
---|---|
本做业属于北航软件工程课程 | 博客园班级博客 |
做业要求请点击连接查看 | 我的项目做业 |
班级:006 | Sample |
GitHub地址 | IntersectProject |
我在这门课程的目标是 | 得到成为一名软件工程师的能力 |
这个做业在哪一个具体方面帮助我实现目标 | 总结过去、规划将来 |
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 90 | 83 |
· Estimate | · 估计这个任务须要多少时间 | 90 | 83 |
Development | 开发 | 830 | 1320 |
· Analysis | · 需求分析 (包括学习新技术) | 30 | 60 |
· Design Spec | · 生成设计文档 | 60 | 40 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
· Design | · 具体设计 | 60 | 120 |
· Coding | · 具体编码 | 240 | 480 |
· Code Review | · 代码复审 | 0 | 0 |
· Test | · 测试(自我测试,修改代码,提交修改) | 360 | 540 |
Reporting | 报告 | 180 | 240 |
· Test Report | · 测试报告 | 30 | 180 |
· Size Measurement | · 计算工做量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 过后总结, 并提出过程改进计划 | 120 | 30 |
合计 | 1100 | 1560 |
1000 <= N <= 500000
0 <= h <= 5000000
拿到题目首先想到暴力求解,两两计算交点,而后去重。可是这样就是纯\(O(n^2)\)的复杂度,必然TLE的。思来想去呢也没有想到本质上改变最坏复杂度\(O(n^2)\)的算法。因而便在网上查了一些资料,发现网上的题目都有一个重要的限定,不存在三线共点。可是咱们这个题目的需求是容许三线共点的,因此并无什么帮助。c++
以后看到了交点个数0 <= h <= 5000000
的限制,感受也许最坏复杂度\(O(n^2)\)的算法并非不可能解的,由于若是有N = 500000
条直线不存在三线共点和平行的话,确实会有\(N(N-1)/2\)个交点,可是之因此交点个数有限制 h <= 5000000
,就说明存在大量的多线共点和平行。git
沿着这个思路想下去,即可以在暴力的\(O(n^2)\)算法基础上考虑将多线共点和平行的状况剪枝掉,剪枝后的具体的时间复杂度比较复杂我没有计算,不过应该是能够知足时间条件的,后文中将对其进行压力测试。github
map<double, set<CLine>>
的_k2lines
中set<CCircle>
的 _circles
里。over
集中。
over
集中其它不平行直线。_insp2shapes
这个map<CPoint, set<CShape>>
数据结构,为交点到通过它的线集的映射。_insp2shapes
里_insp2shapes.size()
即为交点个数。按照代码实现的计划,前后实现三部分功能,实现完即测试,测试经过即提交。测试粒度为pipeline中的函数。测试数据和代码均已上传github。算法
test_input: 构造了4个测试数据,测试输入函数inputShapes的功能,下面为其中一个测试样例,解释见注释:数据结构
测试覆盖单线、常规、共点、平行函数
TEST_METHOD(TestMethod4) { // paralile 数据为两组平行线 // 4 // L 0 0 0 1 // L 0 0 1 1 // L 1 0 1 2 // L 1 0 2 1 //直线通常方程ABC答案集 vector<CLine> ans; ans.push_back(CLine(1, -1, 0)); ans.push_back(CLine(1, -1, -1)); ans.push_back(CLine(1, 0, 0)); ans.push_back(CLine(2, 0, -2)); //直线斜率答案集 vector<CSlope> ans_slope; ans_slope.push_back(CSlope(1.0)); ans_slope.push_back(CSlope(true)); ifstream fin("../test/test4.txt");//读测试输入文件 if (!fin) {//确认读入正确 Assert::AreEqual(132, 0); } //测试开始 CIntersect ins; ins.inputShapes(fin); //获取测试目标数据结构 map<CSlope, set<CLine> > k2lines = ins.getK2Lines(); //对比答案 Assert::AreEqual((int)k2lines.size(), 2); int i = 0; int j = 0; for (map<CSlope, set<CLine> >::iterator mit = k2lines.begin(); mit != k2lines.end(); ++mit, ++i) { Assert::AreEqual(true, mit->first == ans_slope[i]); Assert::AreEqual((int)(mit->second.size()), 2); set<CLine> lines = mit->second; for (set<CLine>::iterator sit = lines.begin(); sit != lines.end(); ++sit, ++j) { Assert::AreEqual(true, ans[j] == *sit); } } }
test_line_intersect: 构造4个测试样例,测试两线交点函数calcShapeInsPoint
,代码略oop
测试覆盖单线、常规、共点、平行性能
test_cnt_intersect: 构造11个测试样例,测试总数函数cntTotalInsPoint
,代码示例学习
测试覆盖单线、常规、共点、平行、浮点精度、内外切、三线切于一点、压力测试
TEST_METHOD(TestMethod9) { // 相切测试,含内切、外切、直线两圆三线切于一点 // 6 // C 0 0 10 // C 4 3 5 // C - 5 0 5 // L 2 14 14 - 2 // L 0 0 0 1 // L - 10 0 - 10 1 ifstream fin("../test/test9.txt"); if (!fin) { Assert::AreEqual(132, 0); } CIntersect ins; ins.inputShapes(fin); int cnt = ins.cntTotalInsPoint(); Assert::AreEqual(9, cnt); // 总数为9 }
可见性能瓶颈在map<CPoint, set<CShape>>
这个_insp2shapes
变量的插入和查找上,经过仔细分析发现,此变量能够优化:
因为此变量的做用是经过给定交点,找到经过此交点的线,因为我能够经过id来惟一肯定一个CShape,因此直接存int就能够了,set<CShape>
能够改为set<int>
其次,这个set是不须要查找的,只须要添加,以及总体copy,因此不须要用set,能够改为vector。set在插入前是须要遍历红黑树的,耗时耗内存。因而原来的map<CPoint, set<CShape>>
改为了map<CPoint, vector<int>>
。
相似的,这个map<CSlope, set<CLine>>
也能够改为map<CSlope, vector<CLine>>
。
众所周知计算机中的浮点数是不能直接比较相等的,常见的浮点数相等的比较方法为
#define EPS 1e-6 double x; double y; if (abs(x-y) < EPS) { cout << "x == y" << endl; }
这种方式保证了在必定的浮点偏差内,两个浮点数认为相等。
在本需求中,涉及到若干浮点数相关类须要重载 <
运算符。其代码须要考虑浮点偏差问题。例如CPoint类的小于运算符代码以下:
bool CPoint::operator < (const CPoint & rhs) const { // 要求仅当 _x < rhs._x - EPS 或 _x < rhs._x + EPS && _y < rhs._y - EPS 时返回true if (_x < rhs._x - EPS || _x < rhs._x + EPS && _y < rhs._y - EPS) { return true; } return false; }
// calculate all intersect points of s1 and s2 // return the points as vector // need: s1, s2 should be CLine or CCircle. // special need: if s1, s2 are CLine. They cannot be parallel. std::vector<CPoint> CIntersect::calcShapeInsPoint(const CShape& s1, const CShape& s2) const { if (s1.type() == "Line" && s2.type() == "Line") { // 直线交点公式,输入要求两线不平行 double x = (s2.C()*s1.B() - s1.C()*s2.B()) / (s1.A()*s2.B() - s2.A()*s1.B()); double y = (s2.C()*s1.A() - s1.C()*s2.A()) / (s1.B()*s2.A() - s2.B()*s1.A()); vector<CPoint> ret; ret.push_back(CPoint(x, y)); return ret; } else { if (s1.type() == "Circle" && s2.type() == "Line") { return calcInsCircLine(s1, s2); } else if (s1.type() == "Line" && s2.type() == "Circle") { return calcInsCircLine(s2, s1); } else { // 两个圆的交点转化为一个圆与公共弦直线的交点 CLine line(s1.D() - s2.D(), s1.E() - s2.E(), s1.F() - s2.F()); return calcInsCircLine(s1, line); } } } // calculate Intersections of one circ and one line // need: para1 is CCirc, para2 is CLine // return a vector of intersections. size can be 0,1,2. std::vector<CPoint> calcInsCircLine(const CShape& circ, const CShape& line) { if (line.k().isInf()) { // 斜率无穷,略 ... } else if (abs(line.k().val() - 0.0) < EPS) { //斜率为0,略 ... } else { vector<CPoint> ret; double k = line.k().val(); double x0 = circ.x0(); double y0 = circ.y0(); double b1 = line.b().val(); double d_2 = (k * x0 - y0 + b1) * (k * x0 - y0 + b1) / (1 + k * k); double d = sqrt(d_2); // 圆心到直线距离 double n; // 半弦长 if (d - circ.r() > EPS) { // not intersect return ret; } else if (circ.r() - d < EPS){ // tangent n = 0.0; } else { // intersect n = sqrt(circ.r() * circ.r() - d_2); } double b2 = x0 / k + y0; double xc = (b2 - b1) / (k + 1 / k); // 弦中点x坐标 double yc = (k * b2 + b1 / k) / (k + 1 / k); // 弦中点y坐标 // 交点坐标 double x1 = xc + n / sqrt(1 + k * k); double x2 = xc - n / sqrt(1 + k * k); double y1 = yc + n * k / sqrt(1 + k * k); double y2 = yc - n * k / sqrt(1 + k * k); ret.push_back(CPoint(x1, y1)); ret.push_back(CPoint(x2, y2)); return ret; } }
// the main pipeline: loop the inputs and fill in _insp2shapes or _insPoints // return the total count of intersect points // need: _k2lines and _circles have been filled int CIntersect::cntTotalInsPoint() { // lines first vector<CLine> over; for (auto mit = _k2lines.begin(); mit != _k2lines.end(); ++mit) { // 遍历平行组 vector<CLine>& s = mit->second; for (auto sit = s.begin(); sit != s.end(); ++sit) { //遍历组内直线 // trick: If the cross point already exists, // we can cut calculation with other lines crossing this point. set<int> can_skip_id; // use this to record which line do not need calculate. for (auto oit = over.begin(); oit != over.end(); ++oit) { // 遍历over集 if (can_skip_id.find(oit->id()) == can_skip_id.end()) { // cannot skip CPoint point = calcShapeInsPoint(*sit, *oit)[0]; // must intersect // 能保证不平行 if (_insp2shapesId.find(point) == _insp2shapesId.end()) { // 全新交点 _insp2shapesId[point].push_back(sit->id()); _insp2shapesId[point].push_back(oit->id()); } else { // cross point already exists 交点已存在 vector<int>& sl = _insp2shapesId[point]; can_skip_id.insert(sl.begin(), sl.end()); // 下次遇到能够跳过不算 _insp2shapesId[point].push_back(sit->id()); } } } } over.insert(over.end(), s.begin(), s.end());// 整个平行组加入over集 } // 后面算圆略 ... }
c++不容许将父类强转为子类,如何更优雅地解决calcShapeInsPoint函数中接收参数是父类类型,可是须要根据不一样子类类型使用不一样方法的问呢?
std::vector<CPoint> calcInsCircLine(const CShape& circ, const CShape& line)
我但愿经过这一个函数,封装所有三类的相交问题,因此在接收的参数上必须采用基类的类型,但函数内部计算时须要使用子类的方法,如何实现呢?
本次使用了c++STL的map和set,底层都是用红黑树实现的,复杂度为O(n)。在讨论交流中发现,c++11标准也新增了相似java中HashSet和HashMap的STL函数,即unordered_map和unordered_set。这个复杂度在好的状况下是O(1)的。下次要记得使用。