软件工程个人项目作业

软件工程个人项目作业

项目 内容
这个作业属于哪个课程 班级博客
这个作业的要求在哪里 作业要求
我在这个课程的目标是 系统地提升软件工程能力
这个作业在哪个具体方面帮助我实现目标 掌握个人软件开发流程
教学班级 006
Github项目地址 https://github.com/Eadral/SE_Individual_Project

PSP

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 10 10
Development 开发
· Analysis · 需求分析 (包括学习新技术) 30 20
· Design Spec · 生成设计文档 5 15
· Design Review · 设计复审 (和同事审核设计文档) 5 5
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10 10
· Design · 具体设计 10 10
· Coding · 具体编码 120 150
· Code Review · 代码复审 20 10
· Test · 测试(自我测试,修改代码,提交修改) 60 240
Reporting 报告
· Test Report · 测试报告 30 30
· Size Measurement · 计算工作量 10 10
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 60 90
合计 370 600

解题思路

求交点

首先考虑最基本的问题,如何求交点。

理想的情况是在数学上推导出一个完整的公式,尽可能减少中间步骤,减少耗时的以及可能带来精度损失的计算,同时有完整的判别公式用于判定点的数量,做到不重不漏。查阅资料后我主要参照Wolfram Mathworld [1] [2] [3],以及博客[4]完成的实现。

去重

题目要求输出点的数目,如果能求出任意两个几何对象的交点的话,那么只要能够去除重复的点即可得到答案。

去重有两种实现方案:

  1. Set(Hash table):每当求出一个交点,就将其插入set中,最终输出set中元素的个数即可。
  2. 排序:每当求出一个交点,将其插入数组尾部,最后对数组进行排序去重,不同元素的个数即是答案。

从理论上看,第一个方案时间复杂度为\(O(m)\),第二个方案时间复杂度为\(O(mlogm)\) (\(m\)为交点个数, 规模为\(O(n^2)\), \(n\)为几何图形个数),但是在现代计算机上实际运行时,缓存是一个影响很大的因素,如果没有经过特定优化,hash table的访存连续性较差,缓存命中率不高,而快速排序能够很好的利用缓存,性能非常好。现代计算机的CPU运算速度很快,计算应用几乎总是memory-bound。而且题目中限制了交点数量不会超过5百万个,排序5百万个点不会超过1s。这里我选择了第二种方案。

设计实现

类设计

考虑编写面向对象程序。这个题目涉及的类有Line,Circle,Point,为了解题应当还有一个Solver。

其中Line,Circle,Point都是不变对象,可以选择使用struct实现,其中Point涉及比较,因此会重载比较运算符,并且在这里需要进行精度判断。因为参数的规模是\(10^5\),曲线最高是二次,因此需要\(10^{10}\)的精度,double类型可以满足要求。

Solver需要获取以上类的对象信息进行求解,这里我选择Solver去组合Line,Circle,和Point。Solver还需要进行IO操作,可以设计成构造时获取IO流,并提供Input和Output接口。

image-20200310153629166

Solve流程

  1. 从流中获取输入,构建Line和Circle数组。

  2. 求交点:遍历Line,Circle数据,两两求交点。交点插入到Point数组中。

    • Line-Line
    • Line-Circle
    • Circle-Circle
  3. 对Point数组进行排序去重。

  4. 将答案输出到流。

    image-20200310153720905

单元测试设计

单元测试应当存在多个粒度,本题没有很复杂的类间关系,其中Line,Circle作为没有方法的不变类,没有必要进行单独的测试。Point只涉及比较,可以在去重排序中完成覆盖。因此单元测试分为以下几类。

1. 交点测试:测试Solver的Line-Line,Line-Circle,Circle-Circle求交点方法分别测试,断言交点数量。

2. 错误测试:对错误输入,异常情况进行测试,断言输出的错误类型。

3. End-to-End测试:向Solver提供输入字符流,对输出字符流进行断言。

性能改进

排序去重

一开始选择的是排序去重方案。随机测试后发现,几何图像数量超过1万后十分缓慢,因为中间过程中交点个数太多了,vector会占据大量的内存,甚至导致程序崩溃。

Hash table

Hash table则不会造成产生大量中间点,改成unordered_set后,虽然不会在中间过程中产生过多的点了,但是hash的计算十分缓慢。而且hash造成的缓存不命中很可能显著影响效率。

Hash 性能较差

image-20200308173911359

排序去重改进

最终我选择了"定期"去重的方法,如果点的数量超过一个指定个数,则进行去重,这样就不会积攒太多的中间点,同时有效利用排序的高效性。

使用大量平行线进行测试后发现,求线交点的运算是性能瓶颈,因此考虑对这一部分进行优化。

Profiling热点

image-20200310130141215

通过将关于单点的运算移动至Line的构造函数中,可以减少求交点时的重复计算,能够一定程度上提升性能。

struct Line {
    long long x1, y1, x2, y2;
    long long dx, dy, x2y1, x1y2, x2y1_x1y2;
    Line(const int x1, const int y1, const int x2, const int y2) noexcept
        : x1(x1), y1(y1), x2(x2), y2(y2) 
    {
        dx = (long long)x1 - (long long)x2;
        dy = (long long)y1 - (long long)y2;
        x2y1 = (long long)x2 * (long long)y1;
        x1y2 = (long long)x1 * (long long)y2;
        x2y1_x1y2 = x2y1 - x1y2;
    }
};

中等规模平行线,和小规模随机数据下,有着较好的性能。

1000000交点随机数据性能

image-20200310094206501

代码说明

数据结构

Circle,Line,Point以不变类为基础,进行了一些优化,并且Point重载了一些运算符。

struct Circle {
    int x, y, r;
    Circle(const int x, const int y, const int r) noexcept : x(x), y(y), r(r) {}
};

struct Line {
    long long x1, y1, x2, y2;
    long long dx, dy, x2y1, x1y2, x2y1_x1y2;
    Line(const int x1, const int y1, const int x2, const int y2) noexcept
        : x1(x1), y1(y1), x2(x2), y2(y2) 
    { /* ... */  }	// 预计算dx,dy等数值
};
struct Point {
    double x, y;
    Point(const double x, const double y) noexcept : x(x), y(y) {}
    friend bool operator==(const Point& lhs, const Point& rhs) noexcept {
        return abs(lhs.x - rhs.x) <= kEps && abs(lhs.y - rhs.y) <= kEps;
    }
    // ... < 运算符重载
};

求解

Solver对以上对象进行组合,并且持有流的引用以进行IO操作。

class Solver {
    istream &in_;
    ostream &out_;

    vector<Line> lines_;
    vector<Circle> circles_;
    vector<Point> points_;
	// ... 
    
 public:
    Solver(/* ... */) {//...}	// 初始化
    
    int Solve();	// 求解,返回值为错误码
    int GetAns();	// 返回答案
        
 private:
    int Input();	// 获取输入,返回值为错误码
    void LineLineIntersect(const Line& a, const Line& b);
    // ......

Solver主要提供Solve,GetAns方法,完成计算和结果的获取。其中Solve进行交点计算,并带有错误处理。

	int Solve() {
        auto err = Input();
        if (err) return err;

        err = GetPointsInLines();
        if (err) return err;
        err = GetPointsInCircles();
        if (err) return err;
        err = GetPointsBetweenLinesAndCircles();
        if (err) return err;

        out_ << GetAns() << endl;

        return 0;
    }

实际的计算则是for循环进行两两求交点。

	int GetPointsInLines() {
        for (auto i = 0; i < n_line_-1; i++) {
            for (auto j = i+1; j < n_line_; j++) {
                LineLineIntersect(lines_.at(i), lines_.at(j));
            }
			// ...
        }
        return 0;
    }

交点计算则是套用数学公式,但是要考虑精度问题,[-kEps, kEps]之间的数被视作0。

    void LineLineIntersect(const Line& a, const Line& b) {
        const auto denominator = a.dx * b.dy - b.dx * a.dy;
        if (denominator == 0) 
            return;
        const auto x_numerator =
            a.x1 * (a.y2 * b.dx + b.x2y1_x1y2) + a.x2 * (a.y1 * -b.dx - b.x2y1_x1y2);
        const auto y_numerator = (b.dy) * (-a.x2y1_x1y2) + (a.dy) *(b.x2y1_x1y2);
        auto x = (double)x_numerator / denominator;
        auto y = (double)y_numerator / denominator;
        points_.emplace_back(x, y);
    }

最后通过排序去重得到答案

		sort(points_.begin(), points_.end());
        const auto new_end = unique(points_.begin(), points_.end());
        points_.erase(new_end, points_.end());	// points_.size()即是答案

单元测试

通过18个单元测试完成了100%覆盖。

Code Coverage Results
image-20200310094044607

通过所有单元测试
image-20200310125027657

代码检查

image-20200310091342220

通过 Code Quality Analysis 的检查,没有警告。

总结

填写PSP可以帮助我进行一些反思,这次项目的测试时间明显超支了,因为一开始把精度问题想的比较简单,到测试阶段才发现问题,花了比较多的时间去验证和修改代码,对整体进度影响比较大,之后应该强制自己再多花一点问题在前期的分析上。这次问题的复杂度比较低,之后面对结对项目,团队项目或者更大的项目应该更加谨慎,多做总结和分析。

通过这次作业,我也开始更加严格的要求自己遵守软件工程规范,例如不再随意提交commit,而是认真写标题和内容并做好分类,期间查阅资料也学到了不少,希望之后在这门课上取得更大收获。

参考资料

[1] https://mathworld.wolfram.com/Circle-CircleIntersection.html

[2] https://mathworld.wolfram.com/Line-LineIntersection.html

[3] https://mathworld.wolfram.com/Circle-LineIntersection.html

[4] https://blog.csdn.net/qq_18509807/article/details/84950132

posted @ 2020-03-10 15:50  哪里有只喵  阅读(297)  评论(2编辑  收藏  举报