我的項目做業$\cdot$求交點個數

我的項目做業\(\cdot\)求交點個數

1、做業要求簡介

本次做業是北航計算機學院軟件工程課程的我的項目做業,我的開發能力對於軟件開發團隊是相當重要的,本項目旨在經過一個求幾何圖形的交點的需求來使學生學會我的開發的經常使用技巧,如PSP方法,需求分析,設計文檔,編碼實現,測試,性能評價等等。java

項目 內容
本做業屬於北航軟件工程課程 博客園班級博客
做業要求請點擊連接查看 我的項目做業
班級:006 Sample
GitHub地址 IntersectProject
我在這門課程的目標是 得到成爲一名軟件工程師的能力
這個做業在哪一個具體方面幫助我實現目標 總結過去、規劃將來

2、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

3、解題思路描述

題目需求簡述

  • 題目需求爲,給定若干直線,求其交點個數
  • 直線條數1000 <= N <= 500000
  • 交點個數0 <= h <= 5000000
  • 運行時長60s

解題思路

拿到題目首先想到暴力求解,兩兩計算交點,而後去重。可是這樣就是純\(O(n^2)\)的複雜度,必然TLE的。思來想去呢也沒有想到本質上改變最壞複雜度\(O(n^2)\)的算法。因而便在網上查了一些資料,發現網上的題目都有一個重要的限定,不存在三線共點。可是咱們這個題目的需求是容許三線共點的,因此並無什麼幫助。c++

以後看到了交點個數0 <= h <= 5000000的限制,感受也許最壞複雜度\(O(n^2)\)的算法並非不可能解的,由於若是有N = 500000條直線不存在三線共點平行的話,確實會有\(N(N-1)/2\)個交點,可是之因此交點個數有限制 h <= 5000000,就說明存在大量的多線共點平行git

沿着這個思路想下去,即可以在暴力的\(O(n^2)\)算法基礎上考慮將多線共點平行的狀況剪枝掉,剪枝後的具體的時間複雜度比較複雜我沒有計算,不過應該是能夠知足時間條件的,後文中將對其進行壓力測試。github

4、設計文檔

(一)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,代碼略oop

    測試覆蓋單線、常規、共點、平行性能

  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
    		}

5、性能改進與消除全部告警

(一) 性能改進

運行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,性能大幅度提高。

以前具體的代碼被採樣到的次數也有所下降,可見修改產生了性能提高。

(二) 消除告警

消除告警前:

消除告警後:

6、代碼說明

(一)浮點數比較處理

衆所周知計算機中的浮點數是不能直接比較相等的,常見的浮點數相等的比較方法爲

#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集
	}
  // 後面算圓略
  ...
}

7、思考

  • 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)的。下次要記得使用。

相關文章
相關標籤/搜索