个人项目作业\(\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. 【关系抽取-R-BERT】定义训练和验证循环

    [关系抽取-R-BERT]加载数据集 [关系抽取-R-BERT]模型结构 [关系抽取-R-BERT]定义训练和验证循环 相关代码 import logging import os import num ...

  2. CCPC-2020 黑龙江省赛——Let’s Get Married

    题意:~~ 思路:题目给出的数字太少了,我们多写几个,就会发现每层最左边的值等于1.2*k(k+1) ,k代表层数,找规律发现如果一个点的坐标为2.(x,y)且|a|+|b|=k,id<=2*k ...

  3. JetBrains Projector 体验

    先来一张最终效果图: JetBrains Projector 是 JetBrains 的"远程开发"解决方案,基于 Client + Server 架构,对标的是微软 VSCode ...

  4. 系统编程-信号-总体概述和signal基本使用

    信号章节 -- 信号章节总体概要 信号基本概念 信号是异步事件,发送信号的线程可以继续向下执行而不阻塞. 信号无优先级. 1到31号信号是非实时信号,发送的信号可能会丢失,不支持信号排队. 31号信号 ...

  5. 生产中常用的获取IP地址方法的总结

    从ifconfig命令的结果中筛选出除了lo网卡之外的所有IPv4地址 centos7 (1)ifconfig | awk '/inet / && !($2 ~ /^127/){pri ...

  6. 【C/C++】面向对象开发的优缺点

    原创文章,转发请注明出处. 面向对象开发的优缺点 面向对象开发 是相对于 面向过程开发 的一种改进思路. 由于流水线式的面相过程开发非常直接,高效.在面对一些简单项目时,只需要几百行,甚至是几十行代码 ...

  7. HTML5与CSS3新增特性笔记

    HTML5 HTML5和HTML事件 注意:行内代码的为H5新增事件 Window事件属性: 针对 window 对象触发的事件(应用到 标签) onafterprint 文档打印之后运行的脚本 on ...

  8. 【macOS】显示/隐藏 允许“任何来源”的应用

    问题产生 在macOS中安装某些版本软件时会提示: "xxx"已损坏,打不开.您应该将它移动到废纸篓. 某些情况下实际上并不是软件已损坏,而是因为macOS对于开发者的验证导致软件 ...

  9. 它来了!!!有史以来第一个64位Visual Studio(2022)预览版将在今夏发布!

    美国时间2021年4月19日,微软产品研发部一位负责人Amanda Silver在其博客上发布一则<Visual Studio 2022>的消息,表示将在今年(2021年)夏天发布Visu ...

  10. 《剑指offer》刷题笔记

    简介 此笔记为我在 leetcode 上的<剑指offer>专题刷题时的笔记整理. 在刷题时我尝试了 leetcode 上热门题解中的多种方法,这些不同方法的实现都列在了笔记中. leet ...