个人项目作业\(\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. 7、MyBatis教程之分页实现

    8.分页实现 1.limit实现分页 思考:为什么需要分页? 在学习mybatis等持久层框架的时候,会经常对数据进行增删改查操作,使用最多的是对数据库进行查询操作,如果查询大量数据的时候,我们往往使 ...

  2. Android Studio 分类整理 res/layout 中的布局文件

    •准备工作 新建一个名为 TestLayouts 的项目: 进入 Project 模式: 来到 TestLayouts/app/src/main/res/layout 文件夹下: •分类整理 layo ...

  3. Dynamics CRM安装教程一:域环境准备

    服务器环境:Windows Service 2016 Stand Windows域是计算机网络的一种形式,其中所有用户账户,计算机,打印机和其他安全主体都在位于称为域控制器的一个或多个中央计算机集群上 ...

  4. Postman 使用小技巧/指南

    一.什么是 Postman(前世今生) Postman 诞生于 2013 年,一开始只是 Abhinav Asthana 着手于解决 API 测试的工具,随着这个工具的使用者和需求迅速激增,Abhin ...

  5. Spring(四)Spring JdbcTemplate&声明式事务

    JdbcTemplate基本使用 01-JdbcTemplate基本使用-概述(了解) JdbcTemplate是spring框架中提供的一个对象,是对原始繁琐的Jdbc API对象的简单封装.spr ...

  6. 【spring cloud hoxton】Ribbon 真的能被 spring-cloud-loadbalancer 替代吗

    背景 早上刷圈看到 Spring Cloud Hoxton.M2 Released 的消息,随手发布到了我的知识星球,过了会有个朋友过来如下问题. 抽取半天时间学习spring-cloud-loadb ...

  7. Seata搭建与分布式事务入门

    在单体架构下,我们大多使用的是单体数据库,通过数据库的ACID特性支持,实现了本地事务.但是在微服务架构下复杂的业务关系中,分布式事务是不可避免的问题之一.Seata是Spring Cloud Ali ...

  8. GO-03-基础

    可见性 Go语言中,使用大小写来决定标识符(常量.变量.类型.接口.结构或函数)是否可以被外部包所调用. 大写字母开头,那么其对象就可以被外部包的代码所使用,如同public. 小写字母开头,则对包外 ...

  9. JDBC_04_使用Properties集合保存JDBC所需配置信息

    使用Properties集合保存JDBC所需配置信息 将JDBC连接所需的配置信息保存在一个配置文件中,然后使用Properties将该信息存储起来,动态的完成JDBC的配置连接 代码: import ...

  10. fastjson反序列化漏洞实际案例利用

    fastjson反序列化rce实际案例利用全过程: 存在问题网站:http://***.com/ 在网站上寻找一些安全漏洞的时候,发现一条json数据包 数据包如下: POST /*** HTTP/1 ...