個人項目——求交點個數
項目 | 內容 |
---|---|
這個作業屬於哪個課程 | 2020春季計算機學院軟件工程(羅傑 任健) |
這個作業的要求在哪里 | 個人項目作業 |
我在這個課程的目標是 | 學習軟工的思想方法,寫出好的軟件並維護 |
這個作業在哪個具體方面幫助我實現目標 | 在寫代碼的過程中熟悉visual studio的功能以及c++的各種函數類的用法,並復習了一些算法知識。 |
班級 | 006 |
項目地址 | github |
一.PSP估計
PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | ||
· Estimate | · 估計這個任務需要多少時間 | 600 | 990 |
Development | 開發 | ||
· Analysis | · 需求分析 (包括學習新技術) | 180 | 480 |
· Design Spec | · 生成設計文檔 | 60 | 60 |
· Design Review | · 設計復審 (和同事審核設計文檔) | 20 | 10 |
· Coding Standard | · 代碼規范 (為目前的開發制定合適的規范) | 20 | 10 |
· Design | · 具體設計 | 40 | 80 |
· Coding | · 具體編碼 | 120 | 240 |
· Code Review | · 代碼復審 | 20 | 30 |
· Test | · 測試(自我測試,修改代碼,提交修改) | 60 | 40 |
Reporting | 報告 | ||
· Test Report | · 測試報告 | 30 | 15 |
· Size Measurement | · 計算工作量 | 30 | 20 |
· Postmortem & Process Improvement Plan | · 事后總結, 並提出過程改進計划 | 20 | 5 |
合計 | 600 | 990 |
二.思路
拿到這個題目的第一想法是暴力求解,對於所有的輸入處理后存起來,並將直線兩兩計算交點,將交點作為key存入map,最后統計map的大小即可。但是在看到數據后猶豫了,對於1000組以下的,這個算法完全沒有問題,而且只要計算過程細心,正確率也能有保證,但是對於性能部分,卻完全不可行。對直線兩兩求交點的復雜度是O(n^2)的,這也就導致在數據大於10000時,速度已非常緩慢,更不用說最大數據500000組了。因此我上網搜索了一些資料,唯一比較接近的是一個用掃描線求線段交點的問題,它的時間復雜度是(k+n)log n,其中n是線段的數量,k是交點的個數,在本次作業中可以滿足時間的要求,因此我決定對於1000組以下的采用暴力計算法,以此來保證算法的正確性,而對於性能部分,采用掃描線算法,提高運行速度。
三.設計實現過程
1.暴力求交點
我的實現方法是構建兩個類分別是Line類和Circle類,其中Line類將輸入的兩個點坐標轉化為直線方程,其形式為$$ Ax+By+c=0 $$而Circle類也做同樣的工作,圓的基本方程形式為 $$ (x-m)2+(y-n)2=r^2 $$在讀入一個數據后用Line或者Circle去存儲相關的變量后,將這個類加入到一個vector中,至此預處理結束。
之后要進行的就是兩兩求交點。首先從Line集合中取兩條不同的直線,然后通過調用方法calculate_line_line來計算兩條直線的交點,思路很簡單,初中數學知識,不多贅述;第二步計算一條直線和一個圓的交點,從Line集合中取出一條直線並從Circle集合中取出一個圓,通過調用方法calculate_line_circle來計算一條直線和一個圓的交點,方法是聯立方程后解方程;最后是計算兩個圓的交點,從Circle集合中取出兩個不同的圓,並調用方法calculate_circle_circle計算出交點,方法的思路也是聯立解方程組。這三步需重復n方次以保證任意兩個幾何圖形都已求過交點,並把這些交點作為Key值insert到map中,map會自動去除重復的交點,最后map的size即為交點個數
單元測試主要測試的計算交點的三個函數是否能正確計算出交點。因此我將calculate_line_line,calculate_line_circle,calculate_circle_circle的返回值設置為int,並在單元測試中構建如下測試:
TEST_METHOD(calculate_line_line)
{
Calculate cal;
Line l1(0, 0, 1, 1);
Line l2(0, 0, 0, 1);
int ret = cal.calculate_line_line(l1, l2);
Assert::AreEqual(ret,(int)1);
Line l3(0, 0, 1, 1);
Line l4(1, 0, 2, 1);
ret = cal.calculate_line_line(l3, l4);
Assert::AreEqual(ret, (int)0);
}
這里測試的是兩條直線的交點是否計算正確,考慮了特殊的平行以及一條直線無斜率的情況,代碼表明能通過測試。
TEST_METHOD(calculate_line_circle)
{
Calculate cal;
Line l1(0, 0, 1, 1);
Circle c1(0, 1, 1);
int ret = cal.calculate_line_circle(l1, c1);
Assert::AreEqual(ret, (int)2);
Line l2(0, 0, 0, 1);
Circle c2(1, 0, 1);
ret = cal.calculate_line_circle(l2, c2);
Assert::AreEqual(ret, (int)1);
Line l3(0, 0, 1, 0);
Circle c3(0, 2, 1);
ret = cal.calculate_line_circle(l3, c3);
Assert::AreEqual(ret, (int)0);
}
這里測試的是一條直線和一個圓的交點是否計算正確,考慮了特殊的直線沒有斜率的情況,以及測試了圓和直線有兩個,一個,零個交點的情況,代碼能通過測試。
TEST_METHOD(calculate_circle_circle)
{
Calculate cal;
Circle c1(0, 0, 1);
Circle c2(1, 1, 1);
int ret = cal.calculate_circle_circle(c1, c2);
Assert::AreEqual(ret, (int)2);
Circle c3(0, 0, 2);
Circle c4(3, 0, 1);
ret = cal.calculate_circle_circle(c3, c4);
Assert::AreEqual(ret, (int)1);
Circle c5(0, 0, 3);
Circle c6(0, 0, 1);
ret = cal.calculate_circle_circle(c5, c6);
Assert::AreEqual(ret, (int)0);
Circle c7(0, 0, 1);
Circle c8(0, 9, 1);
ret = cal.calculate_circle_circle(c7, c8);
Assert::AreEqual(ret, (int)0);
}
這里測試的是兩個圓的交點是否計算正確,考慮了兩個圓有2個,1個,0個交點的情況,同時還考慮了一個圓在另一個圓內部的情況,代碼能通過測試。
至此單元測試結束,而我還進行了一些輸出輸入的測試,通過輸出所有交點的坐標,再與網站GeoGebra的結果比對,發現計算是准確的。
2.掃描線算法
while (!q.empty()) {
Point p = q.top();
q.pop();
if (p.type == 1) {//是左端點
s.insert(p.belong);
set<Line,cmp2>::iterator iter;
iter = s.find(p.belong);
Line l = *iter;
if (iter != s.begin()) {
iter--;
Line l1 = *iter;
iter++;
calculate.calculate_line_line_allinsert(l, l1);
}
iter++;
if (iter != s.end()) {
Line l1 = *iter;
calculate.calculate_line_line_allinsert(l1, l);
}
}
else if (p.type == 2) {//是右端點
set<Line, cmp2>::iterator iter,iter1,del;
iter = s.find(p.belong);
iter1 = iter;
del = iter;
iter1++;
if (iter != s.begin() && iter1 != s.end()) {
iter--;
Line l1 = *iter;
Line l2 = *iter1;
calculate.calculate_line_line_allinsert(l2, l1);
}
s.erase(del);
}
else {//是交點
pointset.insert(p);
set<Line, cmp2>::iterator iter1, iter2, it1, it2;
iter1 = s.find(p.father1);
iter2 = s.find(p.father2);
it1 = iter1;
it2 = iter2;
it1++;
if (it1 != s.end()) {
Line l1 = *it1;
Line l2 = *iter2;
calculate.calculate_line_line_allinsert(l1, l2);
}
if (iter2 != s.begin()) {
it2--;
Line l1 = *iter1;
Line l2 = *it2;
calculate.calculate_line_line_allinsert(l1, l2);
}
}
}
cout << pointset.size() << endl;
掃描線算法雖然寫完了,但卻發現速度並沒有提高,在大量的數據情況下運行速度反而低於暴力,不知道是什么原因,只好只使用暴力的做法。
四.性能分析
首先對於一個500000的數據,如果采用暴力的O(n^2),那么只優化交點到底怎么求來減少每次循環的計算量並不能提高多少性能,為了提高性能需要更換更好的算法。於是我花了大概4小時去學習掃描線(Bentley-Ottmann)算法,只可惜最后不知是set的性能問題還是哪里寫錯了,復雜度總是不對(經常比暴力還慢很多),原本的復雜度應該是O(nlogn+klogn),其中n是直線的數量,k是交點的數量,這個復雜度在5000條邊和500000時應該明顯快過5000*5000,但是事實是比暴力慢很多個數量級,只好放棄。希望能得到助教指點。
下面貼出暴力求解的性能分析圖
對於一個6000組的數據需要運行1分多鍾
占用時間最多的是main函數這是毫無疑問的,因為所有處理基本都在main中,除去main占用時間最多的是計算兩直線的交點,我這組數據直線偏多,因此這個函數占時最多。
其中最耗時的是將交點插入到map中的過程,因此我想能不能改進這里。
因此嘗試將map更換為set,使用一樣的數據,得到如下結果。
發現總時間變短了
每個函數的比例幾乎沒變化,但是時間都變短了一些。
像set中插入point的耗時減少了,那么我們認為set性能更優秀一些,最終選擇了set。
五.代碼說明
代碼的關鍵函數在於計算交點,而這又分為:兩條直線的交點計算,兩個圓的交點計算,以及一個圓和一條直線的交點計算,下面分別來說明這三部分函數。
1.兩條直線的交點計算
int Calculate::calculate_line_line(Line l1,Line l2) {//caculate the crosspoint of the two lines
//int is eazy to test
crosspoint point;
if (l1.A * l2.B == l1.B * l2.A) {
return 0;
}
else {
point.y = (l1.A * l2.C - l1.C * l2.A) / (l1.B * l2.A - l1.A * l2.B);
if (l1.A == 0) {
point.x = (-l2.C - point.y * l2.B) / l2.A;
}
else {
point.x = (-l1.C - point.y * l1.B) / l1.A;
}
pointmap.insert(pair<crosspoint, int>(point, 1));
return 1;
}
}
首先這個函數的返回值是int,這是因為我在單元測試時想要去看它是否計算准確了。而在運行時,我是不管這個int返回值的。我的處理思路是,傳入兩個Line類參數,根據一般式求交點的公式,可以求出一個交點或者無交點將交點作為Key值insert到map中,map會自動判斷這個交點是否已存在,若是存在了,則不再加入map,否則map中加入這個交點。
2.一條直線和一個圓的交點計算
crosspoint point1;
crosspoint point2;
if (l.aNotExist) {
point1.x = l.t;
point2.x = l.t;
double k = ((double)l.t - c.m) * ((double)l.t - c.m);
double r2 = (double)c.r * c.r;
double left = r2 - k;
if (left < 0) {//no result
return 0;
}
else if (left == 0) {//one result
point1.y = c.n;
//pointmap.insert(pair<crosspoint, int>(point1, 1));
Setpoint.insert(point1);
return 1;
}
else {//two result
point1.y = sqrt(left) + c.n;
point2.y = c.n - sqrt(left);
//pointmap.insert(pair<crosspoint, int>(point1, 1));
//pointmap.insert(pair<crosspoint, int>(point2, 1));
Setpoint.insert(point1);
Setpoint.insert(point2);
return 2;
}
}
else {//ax^2+bx+t=0
double a = l.a * l.a + 1;
double b = 2 * ((l.b - c.n) * l.a - c.m);
double t = (double)c.m * c.m + (l.b - c.n) * (l.b - c.n) - (double)c.r * c.r;
double deta = b * b - 4 * a * t;
if (deta > 0) {
point1.x = (sqrt(deta) - b) / (2 * a);
point2.x = (-1 * sqrt(deta) - b) / (2 * a);
point1.y = l.a * point1.x + l.b;
point2.y = l.a * point2.x + l.b;
//pointmap.insert(pair<crosspoint, int>(point1, 1));
//pointmap.insert(pair<crosspoint, int>(point2, 1));
Setpoint.insert(point1);
Setpoint.insert(point2);
return 2;
}
else if (deta == 0) {
point1.x = (b == 0) ? 0 : -1 * b / (2 * a);
point1.y = l.a * point1.x + l.b;
//pointmap.insert(pair<crosspoint, int>(point1, 1));
Setpoint.insert(point1);
return 1;
}
else {
return 0;
}
}
求一條直線和一個圓的交點還是聯立方程求解,需要注意一點,聯立方程后是一個一元二次方程,形式為:$$ ax^2+bx+t=0 $$我們需要根據公式進行計算,其中$$ deta=b^2-4at $$,如果deta的值大於0,那么方程有兩個解,等於0有一個解,否則無解。根據這點對deta分類后分別計算,求出一個x值,然后帶回方程求出y值,將交點坐標作為Key值插入map中。
3.兩個圓的交點計算
int Calculate::calculate_circle_circle(Circle c1, Circle c2) {//calculate the crosspoint of the two circles
crosspoint point1;
crosspoint point2;
if (c2.n == c1.n && c2.m == c1.m) {
return 0;
}
else if (c2.n == c1.n) {
double temp = ((double)c2.m * c2.m - (double)c1.m * c1.m + (double)c2.n * c2.n - (double)c1.n * c1.n + (double)c1.r * c1.r - (double)c2.r * c2.r)
/ ((double)2 * ((double)c2.m - c1.m));
point1.x = temp;
point2.x = temp;
double left = (double)c1.r * c1.r - (temp - c1.m) * (temp - c1.m);
if (left > 0) {
point1.y = sqrt(left) + c1.n;
point2.y = c1.n - sqrt(left);
pointmap.insert(pair<crosspoint, int>(point1, 1));
pointmap.insert(pair<crosspoint, int>(point2, 1));
return 2;
}
else if (left == 0) {
point1.y = c1.n;
pointmap.insert(pair<crosspoint, int>(point1, 1));
return 1;
}
else {
return 0;
}
}
else {
double k = ((double)c1.m - c2.m) / ((double)c2.n - c1.n);
double temp = ((double)c2.m * c2.m - (double)c1.m * c1.m + (double)c2.n * c2.n - (double)c1.n * c1.n + (double)c1.r * c1.r - (double)c2.r * c2.r)
/ ((double)2 * ((double)c2.n - c1.n));
double a = 1 + k * k;
double b = 2 * (k * temp - c1.n * k - c1.m);
double c = (double)c1.m * c1.m + (double)c1.n * c1.n - (double)c1.r * c1.r + temp * temp - 2 * temp * c1.n;
double deta = b * b - 4 * a * c;
if (deta > 0) {
point1.x = (sqrt(deta) - b) / (2 * a);
point2.x = (-1 * sqrt(deta) - b) / (2 * a);
point1.y = point1.x * k + temp;
point2.y = point2.x * k + temp;
pointmap.insert(pair<crosspoint, int>(point1, 1));
pointmap.insert(pair<crosspoint, int>(point2, 1));
return 2;
}
else if (deta == 0) {
point1.x = (b == 0) ? 0 : -1 * b / (2 * a);
point1.y = point1.x * k + temp;
pointmap.insert(pair<crosspoint, int>(point1, 1));
return 1;
}
else {
return 0;
}
}
}
兩個圓的交點求法同一條直線和一個圓,只不過參數更多更復雜而已,這完全是數學推導,因此不做過多贅述。
六.消除警告
已消除Code Quality Analysis 中的所有警告