zoukankan      html  css  js  c++  java
  • 软件工程个人项目作业

    软件工程个人项目作业

    项目 内容
    这个作业属于哪个课程 班级博客
    这个作业的要求在哪里 作业要求
    我在这个课程的目标是 系统地提升软件工程能力
    这个作业在哪个具体方面帮助我实现目标 掌握个人软件开发流程
    教学班级 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

  • 相关阅读:
    加法原理和乘法原理
    布尔矩阵
    Codeforces Round #603 (Div. 2) A. Sweet Problem
    Codeforces Round #603 (Div. 2) D. Secret Passwords 并查集
    poj1611The Suspects并查集
    poj 2236 Wireless Network 并查集
    求斐波那契数的python语言实现---递归和迭代
    python语言实现阶乘的两种方法---递归和迭代
    栈实现二进制转十进制
    栈的基本操作
  • 原文地址:https://www.cnblogs.com/wheremeow/p/12456178.html
Copyright © 2011-2022 走看看