本次做業是北航計算機學院軟件工程課程的我的項目做業,我的開發能力對於軟件開發團隊是相當重要的,本項目旨在經過一個求幾何圖形的交點的需求來使學生學會我的開發的經常使用技巧,如PSP方法,需求分析,設計文檔,編碼實現,測試,性能評價等等。java
項目 | 內容 |
---|---|
本做業屬於北航軟件工程課程 | 博客園班級博客 |
做業要求請點擊連接查看 | 我的項目做業 |
班級:006 | Sample |
GitHub地址 | IntersectProject |
我在這門課程的目標是 | 得到成爲一名軟件工程師的能力 |
這個做業在哪一個具體方面幫助我實現目標 | 總結過去、規劃將來 |
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 |
1000 <= N <= 500000
0 <= h <= 5000000
拿到題目首先想到暴力求解,兩兩計算交點,而後去重。可是這樣就是純\(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
map<double, set<CLine>>
的_k2lines
中set<CCircle>
的 _circles
裏。over
集中。
over
集中其它不平行直線。_insp2shapes
這個map<CPoint, set<CShape>>
數據結構,爲交點到通過它的線集的映射。_insp2shapes
裏_insp2shapes.size()
即爲交點個數。按照代碼實現的計劃,前後實現三部分功能,實現完即測試,測試經過即提交。測試粒度爲pipeline中的函數。測試數據和代碼均已上傳github。算法
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); } } }
test_line_intersect: 構造4個測試樣例,測試兩線交點函數calcShapeInsPoint
,代碼略oop
測試覆蓋單線、常規、共點、平行性能
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 }
可見性能瓶頸在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>>
。
衆所周知計算機中的浮點數是不能直接比較相等的,常見的浮點數相等的比較方法爲
#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; }
// 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集 } // 後面算圓略 ... }
c++不容許將父類強轉爲子類,如何更優雅地解決calcShapeInsPoint函數中接收參數是父類類型,可是須要根據不一樣子類類型使用不一樣方法的問呢?
std::vector<CPoint> calcInsCircLine(const CShape& circ, const CShape& line)
我但願經過這一個函數,封裝所有三類的相交問題,因此在接收的參數上必須採用基類的類型,但函數內部計算時須要使用子類的方法,如何實現呢?
本次使用了c++STL的map和set,底層都是用紅黑樹實現的,複雜度爲O(n)。在討論交流中發現,c++11標準也新增了相似java中HashSet和HashMap的STL函數,即unordered_map和unordered_set。這個複雜度在好的狀況下是O(1)的。下次要記得使用。