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

  • 相关阅读:
    Eclipse 导入项目乱码问题(中文乱码)
    sql中视图视图的作用
    Java基础-super关键字与this关键字
    Android LayoutInflater.inflate(int resource, ViewGroup root, boolean attachToRoot)的参数理解
    Android View和ViewGroup
    工厂方法模式(java 设计模式)
    设计模式(java) 单例模式 单例类
    eclipse乱码解决方法
    No resource found that matches the given name 'Theme.AppCompat.Light 的完美解决方案
    【转】使用 Eclipse 调试 Java 程序的 10 个技巧
  • 原文地址:https://www.cnblogs.com/wheremeow/p/12456178.html
Copyright © 2011-2022 走看看