• 软件工程--个人项目作业


    项目 内容
    课程 软件工程
    作业 个人项目作业
    教学班级 005
    GitHub地址 https://github.com/yankuai/Interset.git

    PSP 表格

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

    解题思路描述

    总体思路

    封装point,line和circle类。其中point类可以继承自STL pair<double,double>,line和circle类的属性应该体现其表达式的参数。

    line一般式: (ax+by+c=0)

    circle公式:((x-x_0)^2+(y-y_0)^2=r^2)

    暴力算法寻找交点,计算每两个图形的交点,将交点放进STL set来去重,复杂度(O(n^2))

    直线与直线的交点

    直线的一般表示:

    (f(x) : ax + by + c = 0)

    已知直线上两点((x_0,y_0),(x_1,y_1)),可以得到一般式的参数:

    (a = y_0 – y_1)

    (b = x_1 – x_0)

    (c = x_0*y_1 – x_1*y_0)

    以上两点坐标向直线一般式的转换在数据输入阶段完成。遍历直线计算交点时,由一般式计算直线交点的方程为:

    (x = (b_0*c_1 – b_1*c_0)/D)

    (y = (a_1*c_0 – a_0*c_1)/D)

    (D = a_0*b_1 – a_1*b_0)

    使用一般式的好处是,不需要单独考虑斜率为0的情况。

    圆与圆的交点

    回顾一下圆的两种表示方法:

    圆的标准方程:((x−x_0)^2+(y−y_0)^2=r^2)

    圆的参数方程:

    [egin{equation} egin{cases} x=x_0+r⋅cosθ\ y=y_0+r⋅sinθ end{cases} end{equation}]

    将第一个圆的参数方程带入第二个圆的标准方程:
    ((x_1+r_1 cosθ−x_2)^2+(y_1+r_1 sinθ−y-2)^2=r_2^2)
    展开后得到:
    (2 r_1(x_1−x_2)cosθ+2 r_1(y_1−y_2)sinθ=r_2^2−r_1^2−(x_1−x_2)^2−(y_1−y_2)^2)

    令:
    (a=2r_1(x_1−x_2))

    (b=2r_1(y_1−y_2))

    (c=r_2^2−r_1^2−(x_1−x_2)^2−(y_1−y_2)^2)

    原式变为:
    (acosθ+bsinθ=c)

    (cosθ=x,sinθ=sqrt{1−x^2}) ,关于sin**θsinθ 的正负后面再判断。
    代入方程,得到,(ax+bsqrt{1−x^2}=c)
    移项再两边平方,((ax−c)^2=b^2(1−x^2))
    整理得,((a^2+b^2)x^2−2acx+c^2−b^2=0),解一元二次方程得到(x,sinθ,cosθ)

    (sinθ)(cosθ)代回到第一个圆的参数方程,得到交点的坐标。

    接下来需要注意,将求得交点带入第二个圆的方程验证,如果不在第二个圆上,说明(sinθ)是负数,需要对交点做调整。另一种特殊情况是,如果确定两个不同交点,解出来的(cosθ)只有一个,说明对应的(sinθ)值一正一负。

    参考博客:https://www.cnblogs.com/AOQNRMGYXLMV/p/5098304.html

    直线与圆的交点

    求交点步骤如下

    1. 首先求出圆心c在直线l 上的投影点pr的坐标

    2. 计算向量p1p2的单位向量e, 再求出base的长度, 进而求出向量base

    3. 最后,以pr作为起点, 向正or负方向加上该向量, 就可以得到圆与直线的交点了

    参考博客:http://www.pianshen.com/article/6764257/

    设计实现过程

    代码组织(几个类,几个函数,关系,关键函数流程图,单元测试设计)

    代码组织

    Point类

    point不仅代表点坐标,还可以表示向量。在计算直线和圆的交点时,我运用了很多向量运算求圆心到直线的投影等,所以重写了operator方法,方便向量运算。

    class Point : public pair<double, double> {
    public:
    	Point() {}
    	Point(double x, double y);
    	Point operator - (const Point& p);
    	bool operator ==(const Point& p);
    	void operator=(const Point& point);
    	Point operator/(const double& d);
    };
    

    Line类

    a,b,c是直线一般式的参数,e是直线的单位向量,p1,p2是确定直线的两点。

    getIntersection_ll是求两直线交点的方法,传入set的指针,方法内直接将交点加入。

    class Line {
    public:
    	double a;
    	double b;
    	double c;
    	Point e;
    	Point p1;
    	Point p2;
    
    	Line() {}
    	Line(Point source, Point target);
    	void operator=(const Line& line);
    	int getIntersection_ll(set<Point>* intersections, Line l1, Line l2);
    };
    

    Circle类

    getIntersection_cc是求两圆交点的方法。

    class Circle {
    public:
    	Point c;
    	double r;
    
    	Circle() {}
    	Circle(Point c, double r) :c(c), r(r) {}
    	void operator=(const Circle& circle);
    	int getIntersection_cc(set<Point>* intersections, Circle c1, Circle c2);
    };
    

    getIntersection_cl函数

    求直线与圆交点的方法,不在line和circle类之中。其实把求两直线和两圆交点的方法分别放进line和circle类有些多此一举,因为参数还是会传入两个被观察的几何体,这两个方法类似于类中的静态方法,不用创建实例也可以用。

    Geometry结构体

    包含图形的标签(圆,直线),和图形自身的类。是为了实现将line和circle类存入一个vector容器,这样在遍历图形的时候更方便。

    enum GType { L, C };
    
    struct Geometry {
    	GType Gflag;
    	union {
    		Line lObj;
    		Circle cObj;
    	};
    	Geometry(Line l) {
    		Gflag = L;
    		lObj = l;
    	}
    	Geometry(Circle c) {
    		Gflag = C;
    		cObj = c;
    	}
    	void getObj(Line& obj) {
    		if (Gflag == L) {
    			obj = lObj;
    		}
    	}
    	void getObj(Circle& obj) {
    		if (Gflag == C) {
    			obj = cObj;
    		}
    	}
    };
    

    参考博客:https://zouzhongliang.com/index.php/2019/06/22/stlrongqizhongcunfangbutongleixingshixianfangfa/

    单元测试设计

    单元测试考虑了十种情况,包括直线与直线相交(相交、平行、斜率不存在),圆与圆相交(相离、内含、相切、相交),圆与直线相交(相离、相切、相交)。在这些情况下均能通过测试。

    性能改进思路

    性能分析图

    消耗最大的函数

    将近90%时间都花在set的insert函数,其中两个点的比较运算最耗时。

    改进思路

    set的实现原理是红黑树,很难进一步优化。所以我尽量减少放进set的交点数,比如圆相切时,只把交点放进set一次。

    代码说明

    直线与直线交点

    int Line::getIntersection_ll(set<Point>* intersections, Line l1, Line l2) {
    	double D = l1.a * l2.b - l2.a * l1.b;
    	if (D == 0) return 0;
    	Point p = { (l1.b * l2.c - l2.b * l1.c) / D, (l2.a * l1.c - l1.a * l2.c) / D };
    	p.first = (float)p.first;
    	p.second = (float)p.second;
    	intersections->insert(p);
    	return 1;
    }
    

    圆与圆交点

    int Circle::getIntersection_cc(set<Point>* intersections, Circle c1, Circle c2) {
    	double r1 = c1.r, r2 = c2.r;
    	double x1 = c1.c.first, x2 = c2.c.first, y1 = c1.c.second, y2 = c2.c.second;
    	double d = length(c1.c - c2.c);		//dist of circle center
    
    	if (dcmp(fabs(r1 - r2) - d) > 0) return -1;
    	if (dcmp(r1 + r2 - d) < 0) return 0;
    
    	double d2 = sqr(d);	//d*d
    	double a = 2 * r1 * (x1 - x2);
    	double b = 2 * r1 * (y1 - y2);
    	double c = r2 * r2 - r1 * r1 - d * d;
    	double p = a * a + b * b;
    	double q = -2 * a * c;
    	double r = c * c - b * b;
    
    	double cosa, sina, cosb, sinb;
    	//one intersection
    	if (dcmp(d - (r1 + r2)) == 0 || dcmp(d - fabs(r1 - r2)) == 0) {
    		cosa = -q / p / 2;													//被除数 p = a2 + b2 > 0
    		sina = sqrt(1 - sqr(cosa));
    		Point p(x1 + c1.r * cosa, y1 + c1.r * sina);
    		if (!onCircle(p, c2)) p.second = y1 - c1.r * sina;
    		p.first = (float)p.first;
    		p.second = (float)p.second;
    		intersections->insert(p);
    		return 1;
    	}
    	//two intersections
    	double delta = sqrt(q * q - 4 * p * r);
    	cosa = (delta - q) / p / 2;
    	cosb = (-delta - q) / p / 2;
    	sina = sqrt(1 - sqr(cosa));
    	sinb = sqrt(1 - sqr(cosb));
    	Point p1(x1 + c1.r * cosa, y1 + c1.r * sina);
    	Point p2(x1 + c1.r * cosb, y1 + c1.r * sinb);
    	if (!onCircle(p1, c2)) p1.second = y1 - c1.r * sina;
    	if (!onCircle(p2, c2)) p2.second = y1 - c1.r * sinb;
    	if (p1 == p2) p1.second = y1 - c1.r * sina;
    	p1.first = (float)p1.first;
    	p1.second = (float)p1.second;
    	p2.first = (float)p2.first;
    	p2.second = (float)p2.second;
    	intersections->insert(p1);
    	intersections->insert(p2);
    	return 2;
    }
    

    直线与圆交点

    int getIntersection_cl(set<Point>* intersections, Circle c, Line l) {
    	if (dcmp(disLine(c.c, l) - c.r) == 0) { return 0; }
    	Point Base = vbase(c, l);
    	Point pr = prxy(c, l);
    	Point inter1 = { Base.first + pr.first, Base.second + pr.second };
    	Point inter2 = { pr.first - Base.first, pr.second - Base.second };
    	inter1.first = (float)inter1.first;
    	inter1.second = (float)inter1.second;
    	inter2.first = (float)inter2.first;
    	inter2.second = (float)inter2.second;
    	if (inter1 == inter2) {
    		intersections->insert(inter1);
    		return 1;
    	}
    	else {
    		intersections->insert(inter1);
    		intersections->insert(inter2);
    		return 2;
    	}
    }
    
    double disLine(Point p, Line l) {		//点到直线距离
    	double a = l.a, b = l.b, c = l.c;
    	double x = p.first, y = p.second;
    	return fabs(a * x + b * y + c) / sqrt(a * a + b * b);	//a,b不同时为0
    }
    
    Point vbase(Circle c, Line l) {	//向量base
    	double r = c.r;
    	double base = sqrt(r * r - sqr(disLine(c.c, l)));
    	return Point(l.e.first * base, l.e.second * base);
    }
    
    Point prxy(Circle c, Line l) {		//投影点pr的坐标
    	Point A = l.p1;
    	Point B = l.p2;
    	Point O = c.c;
    	Point AB = B - A;
    	Point AO = O - A;
    	double L = dot(AB, AO) / length(AB);
    	Point Apr(l.e.first * L, l.e.second * L);
    	return Point(A.first + Apr.first, A.second + Apr.second);
    }
    

    精度问题

    可以发现,在进行交点比较的时候,使用了dcmp函数,而不是直接比较。dcmp允许比较数有些许误差,还是能返回相等的结果。造成大量的除法和开方运算造成了精度损失,使相同的交点坐标出现很小的差别。

    int dcmp(double x) {
    	if (fabs(x) < eps) return 0;
    	return x < 0 ? -1 : 1;
    }
    

    另外在把交点加入set的时候,将double类型的坐标转换成float以降低精度,避免有细微差别的交点在集合里被当成两个不同的点。

    我认为精度问题体现在两个地方,其一在计算交点坐标的时候,难免会有精度损失;其二在把交点放入集合的时候,比较交点时的比较精度会更高。是计算的低精度和比较的高精度造成的不适配,导致相同交点会在集合里出现两次。

    • 方法一是提高计算精度,举例:

    尽量把3.0计算成3.0000000000001而不是3.00001,这样在double省略掉后面位数时不会产生大误差。

    • 方法二是降低比较精度,这里包括判断交点是否相等的显示比较(dcmp函数),以及set内部去重的隐式比较。对于降低set内的比较精度,可以在数据加入set前用强制类型转换降低数据的精度,或者不改变数据而重写set比较函数。举例:

    如果计算精度没有提升,已经把3.0算成3.00001,那比较时只看小数点后4位,3.00001就还是3.0。

    这两个例子的精确度只为做示范,实际比较函数要估算到小数点后几位,是很值得研究的问题,我还没有找到答案(肯定不是例子的位数啦)。亲测在我选用不同精确度,计算上千条直线交点时,有不同的结果。

    代码分析截图

  • 相关阅读:
    记录未完成
    java8时间有关新特性
    《java多线程编程核心技术》----simpleDateFormat非线程安全
    基于JavaScript的表格设计:按序添加或删除班级的学生信息
    BOOK
    Android攻城狮Dialog
    Android攻城狮重新认识Toast
    Android攻城狮使用LogCat方式调试程序
    Android攻城狮属性动画赏析
    Android攻城狮布局动画
  • 原文地址:https://www.cnblogs.com/mollygarden/p/12456930.html
Copyright © 2020-2023  润新知