个人项目作业\(\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. P1781_宇宙总统(JAVA语言)

    //水题 题目背景 宇宙总统竞选 题目描述 地球历公元6036年,全宇宙准备竞选一个最贤能的人当总统,共有n个非凡拔尖的人竞选总统,现在票数已经统计完毕,请你算出谁能够当上总统. 输入输出格式 输入格 ...

  2. 攻防世界 reverse BABYRE

    BABYRE   XCTF 4th-WHCTF-2017 int __cdecl main(int argc, const char **argv, const char **envp) { char ...

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

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

  4. 从I/O多路复用到Netty,还要跨过Java NIO包

    本文是Netty系列第4篇 上一篇文章我们深入了解了I/O多路复用的三种实现形式,select/poll/epoll. 那Netty是使用哪种实现的I/O多路复用呢?这个问题,得从Java NIO包说 ...

  5. 计划任务统一集中管理系统cronsun(替代crontab)

    一.背景 crontab 是 Linux 系统里面最简单易用的定时任务管理工具,相信绝大多数开发和运维都用到过,很多业务系统的定时任务都是通过 crontab 来定义的,时间长了后会发现存在很多问题: ...

  6. Istio 故障注入之延时(fixedDelay)

    Istio 故障注入 Istio 故障注入与其他在网络层引入错误(例如延迟数据包或者直接杀死 Pod)的机制不同,Istio 允许在应用程序层注入故障.这使得可以注入更多相关的故障,比如 HTTP 错 ...

  7. 基于vite2的react脚手架

    vite-react-boilerplate 开发编译 yarn start 启动开发 yarn build 启动编译 代码质量和风格 husky/lint-staged/eslint/prettie ...

  8. ES9的新特性:异步遍历Async iteration

    ES9的新特性:异步遍历Async iteration 目录 简介 异步遍历 异步iterable的遍历 异步iterable的生成 异步方法和异步生成器 简介 在ES6中,引入了同步iteratio ...

  9. python程序—一键部署keepalived+lvs

    一个DS 两个RS keepalived端在/root下准备好已经修改好的配置文件 import paramiko # keepalived端 需要修改的信息 keepalived_ip='192.1 ...

  10. shell脚本 5 sed和awk

    文本处理三剑客 在 Shell 下使用这些正则表达式处理文本最多的命令有下面几个工具: 命令 描述 grep 默认不支持扩展表达式,加-E 选项开启 ERE.如果不加-E 使用花括号要加转义符\{\} ...