个人项目作业\(\cdot\)求交点个数

一、作业要求简介

本次作业是北航计算机学院软件工程课程的个人项目作业,个人开发能力对于软件开发团队是至关重要的,本项目旨在通过一个求几何图形的交点的需求来使学生学会个人开发的常用技巧,如PSP方法,需求分析,设计文档,编码实现,测试,性能评价等等。

项目 内容
本作业属于北航软件工程课程 博客园班级博客
作业要求请点击链接查看 个人项目作业
班级:006 Sample
GitHub地址 IntersectProject
我在这门课程的目标是 获得成为一名软件工程师的能力
这个作业在哪个具体方面帮助我实现目标 总结过去、规划未来

二、PSP表格

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
  • 运行时长60s

解题思路

拿到题目首先想到暴力求解,两两计算交点,然后去重。但是这样就是纯\(O(n^2)\)的复杂度,必然TLE的。思来想去呢也没有想到本质上改变最坏复杂度\(O(n^2)\)的算法。于是便在网上查了一些资料,发现网上的题目都有一个重要的限定,不存在三线共点。但是我们这个题目的需求是允许三线共点的,所以并没有什么帮助。

之后看到了交点个数0 <= h <= 5000000的限制,感觉也许最坏复杂度\(O(n^2)\)的算法并不是不可能解的,因为如果有N = 500000条直线不存在三线共点平行的话,确实会有\(N(N-1)/2\)个交点,但是之所以交点个数有限制 h <= 5000000,就说明存在大量的多线共点平行

沿着这个思路想下去,便可以在暴力的\(O(n^2)\)算法基础上考虑将多线共点平行的情况剪枝掉,剪枝后的具体的时间复杂度比较复杂我没有计算,不过应该是可以满足时间条件的,后文中将对其进行压力测试。

四、设计文档

(一)PipeLine

PreProcess

  • ReadShape:读取文件接收全部输入的直线和圆
  • Shape construct:根据输入构建形状对象,计算直线斜率。
  • Classified by Slope:按斜率将直线分组存起来。

CalcIntersect

  • CalcLines:计算所有直线之间的交点:

    • 依次考虑每个平行组,按每条线遍历计算交点。平行组内的线不用计算交点。
    • 查交点表,如果存在,就可以不求同一交点的其他线了。

      交点表:Map<点,Set<线>>

      维护交点表:新增的交点加入交点表,线加入表中对应的线集
  • CalcCircles:所有线算完后,再一个个遍历圆。暴力求其与之前图形的全部交点。
  • 计算圆与直线的交点时,可以按如下方法剪枝:

    考虑圆与一族平行线的交点,将平行线族的截距排序为b1,b2,b3 \(\cdots\)

    若bi开始与圆相离,则大于i的线一定相离,反正小于的情况亦然。

(二)类间关系图UML

  • CIntersect类:实现控制流,方法包含输入计算两图形交点计算交点总数
  • CShape类:图形类基类,为每个图形实例创建唯一id
  • CLine类和CCircle类:继承图形类基类,作用为表示形状代数方程参数。
  • 直线方程两种表示方法
    • 一般方程:\(Ax + By +C = 0\)
    • 斜截方程:\(y = kx + b\)
    • 圆方程两种表示
      • 一般方程: \(x^2 + y^2 + Dx + Ey +F = 0\)
      • 标准方程: \((x-x_0)^2 + (y-y_0)^2 = r^2\)
  • CSlope类和CBias类:为解决斜率无穷大设计,isInf和isNan为true时表示直线的斜率为无穷,此时k和b的具体值无效。由于要按斜率分组,CSlope要实现小于运算符。
  • CPoint类:表示交点,作为map的key,需要实现小于运算符。

(三)关键函数

  • inputShapes: 处理输入函数,直线按斜率分组,放到map<double, set<CLine>>_k2lines

    圆直接放到set<CCircle>_circles里。
  • calcShapeInsPoint:求两个图形交点的函数,分三种情况,返回点的vector。
    • 直线与直线
    • 直线与圆
    • 圆与圆
  • cntTotalInsPoint: 求所有焦点的函数,按先直线后圆的顺序依次遍历求焦点。已经遍历到的图形加入一个over集中。
    • 直线两个剪枝方法:

      • 砍平行:依次加入每个平行组,不需计算组内直线交点,只需遍历over集中其它不平行直线。
      • 砍共点:假若ABC共点,按ABC的顺序遍历,先计算了AB,交点为P;之后计算AC时发现交点也是P,则无需计算BC交点。方法为维护_insp2shapes这个map<CPoint, set<CShape>>数据结构,为交点到经过它的线集的映射。
    • 再依次遍历圆,暴力求焦点。加到_insp2shapes
    • 函数返回_insp2shapes.size()即为交点个数。

(四)测试设计

按照代码实现的计划,先后实现三部分功能,实现完即测试,测试通过即提交。测试粒度为pipeline中的函数。测试数据和代码均已上传github。

  1. 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);
    }
    }
    }
  2. test_line_intersect: 构造4个测试样例,测试两线交点函数calcShapeInsPoint,代码略

    测试覆盖单线、常规、共点、平行

  3. 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
    }

五、性能改进与消除所有告警

(一) 性能改进

运行VS2017的性能探测器,查看自己代码的性能瓶颈。

可见运行总耗时38s,最耗时的函数为cntTotalInsPoint, 下面仔细分析此函数,找出性能瓶颈。

分析:

可见性能瓶颈在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>>

修改后的性能分析

可以看出,运行总时间由38减少到了27,性能大幅度提升。

之前具体的代码被采样到的次数也有所降低,可见修改产生了性能提升。

(二) 消除告警

消除告警前:

消除告警后:

六、代码说明

(一)浮点数比较处理

众所周知计算机中的浮点数是不能直接比较相等的,常见的浮点数相等的比较方法为

#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;
}

(二)求两线交点:直线与直线 or 直线与圆 or 圆与圆

// 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)

    我希望通过这一个函数,封装全部三类的相交问题,所以在接收的参数上必须采用基类的类型,但函数内部计算时需要使用子类的方法,如何实现呢?

    • 通过传指针能做到,把参数设为父类的指针,然后强转为子类的指针,但是不太方便,也不太优雅。
    • 通过传引用也能实现,利用虚函数的多态特性动态调用对应的子类的函数。但需要在基类里写完全用不到的方法,失去了封装性。如在CShape类里写getA()(目前采用的实现方式)
  • 本次使用了c++STL的map和set,底层都是用红黑树实现的,复杂度为O(n)。在讨论交流中发现,c++11标准也新增了类似java中HashSet和HashMap的STL函数,即unordered_map和unordered_set。这个复杂度在好的情况下是O(1)的。下次要记得使用。

个人项目作业$\cdot$求交点个数的更多相关文章

  1. Rikka with Mista 线段树求交点个数

    由于上下线段是不可能有交点的 可以先看左右线段树,按照y递增的顺序,对点进行排序. 升序构造,那么对于从某一点往下的射线,对于L,R进行区间覆盖,线段交点个数就是单点的被覆盖的次数. 降序构造,那么对 ...

  2. SE_Work2_交点个数

    项目 内容 课程:北航-2020-春-软件工程 博客园班级博客 要求:求交点个数 个人项目作业 班级:005 Sample GitHub地址 IntersectProject 一.PSP估算 在开始实 ...

  3. BUAA 2020 软件工程 个人项目作业

    BUAA 2020 软件工程 个人项目作业 Author: 17373051 郭骏 项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 个人项目作业 ...

  4. 结对项目:求交点pro

    [2020 BUAA 软件工程]结对项目作业 项目 内容 课程:北航2020春软件工程 博客园班级博客 作业:阅读并撰写博客回答问题 结对项目作业 我在这个课程的目标是 积累两人结对编程过程中的经验 ...

  5. BUAA 软工 结对项目作业

    1.相关信息 Q A 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 结对项目作业 我在这个课程的目标是 系统地学习软件工程开发知识,掌握相关流程和技术,提升 ...

  6. BUAA SE 个人项目作业

    项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 个人项目作业 我在这个课程的目标是 通过个人项目实践熟悉个人开发流程 一.在文章开头给出教学班级和 ...

  7. BUAA软件工程个人项目作业

    BUAA软件工程个人项目作业 项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 个人项目作业 我在这个课程的目标是 学习软件开发的流程 这个作业在哪 ...

  8. BUAA软工-结对项目作业

    结对项目作业 项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 结对项目作业 我在这个课程的目标是 通过这门课锻炼软件开发能力和经验,强化与他人合作 ...

  9. 2020BUAA软工结伴项目作业

    2020BUAA软工结伴项目作业 17373010 杜博玮 项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 结伴项目作业 我在这个课程的目标是 学 ...

随机推荐

  1. 攻防世界 reverse 进阶 10 Reverse Box

    攻防世界中此题信息未给全,题目来源为[TWCTF-2016:Reverse] Reverse Box 网上有很多wp是使用gdb脚本,这里找到一个本地还原关键算法,然后再爆破的 https://www ...

  2. J. Cole 的 InnoDB 系列 - 1. 学习 InnoDB - 深入探索核心原理之旅

    原文地址:https://blog.jcole.us/2013/01/02/on-learning-innodb-a-journey-to-the-core/,本系列翻译会在其基础上扩展一些 MySQ ...

  3. ApiTesting全链路接口自动化测试框架 - 实战应用

    场景一.添加公共配置 我们在做自动化开始的时候,一般有很多公共的环境配置,比如host.token.user等等,如果这些放在用例中,一旦修改,将非常的不便.麻烦(尤其切换环境). 所以这里我们提供了 ...

  4. python中zip函数的使用

    zip(*iterables) zip可以将多个可迭代对象组合成一个迭代器对象,通过迭代取值,可以得到n个长度为m的元组.其中n为长度最短可迭代对象的元素个数,m为可迭代对象的个数.并且每个元组的第i ...

  5. JS中EventLoop、宏任务与微任务的个人理解

    为什么要EventLoop? JS 作为浏览器脚本语言,为了避免复杂的同步问题(例如用户操作事件以及操作DOM),这就决定了被设计成单线程语言,而且也将会一直保持是单线程的.而在单线程中若是遇到了耗时 ...

  6. C/C++ 中的算术及其陷阱

    目录 概述 C/C++ 整数的阴暗角落 整型字面量 整型提升与寻常算术转换 算术溢出检测 位运算技巧 总结 参考 概述 无符号数和有符号数是通用的计算机概念,具体到编程语言上则各有各的不同,程序员是解 ...

  7. PAT (Advanced Level) Practice 1011 World Cup Betting (20 分) 凌宸1642

    PAT (Advanced Level) Practice 1011 World Cup Betting (20 分) 凌宸1642 题目描述: With the 2010 FIFA World Cu ...

  8. 【Android】修改Android Studio的SDK位置

    解决SDK占用C盘空间问题 由于Android Studio默认会将环境下载到C盘,会导致C盘空间被大量占用. 对于C盘窘迫的童鞋非常不友好. 可以通过修改SDK位置的方式缓解C盘空间焦虑. 打开&q ...

  9. Spring(七篇)

    (一)Spring 概述 (二)Spring Bean入门介绍 (三)Spring Bean继续入门 (四)Spring Bean注入方试 (五)Spring AOP简述 (六)Spring AOP切 ...

  10. 【接入指南】一个Demo带你玩转华为帐号服务

    在<接入指南:一文带你了解华为帐号服务>中已经给大家介绍了华为帐号服务有哪些优势,如一键授权登录华为全场景共享.共享华为帐号所有用户资源.帐号安全可靠.接入方便快捷等,以及为什么能帮助开发 ...