上次的博客寫到一半宿舍停電了。。。。然而今天想起來補充完的時候發現博客園並沒有自動保存哦,微笑。
最近對問題
首先來看最近對問題,最近對問題描述的就是在包含n個端的集合中找到距離最近的兩個點,當然問題也可以定義在多維空間中,但是這里只是跟隨書上的思路實現了二維情況下的最近對問題。假設所有討論的點是以標准的笛卡爾坐標形式(x,y)給出的,那么在兩個點Pi=(Xi,Yi)和Pj=(Xj,Yj)之間的距離是標准的歐幾里得距離:
d(Pi,Pj)=sqrt( (X1-X2)2+(Y1-Y2)2 )
蠻力法的思路就是計算出所有的點之間的距離,然后找出距離最小的那一對,在這里增加效率的一種方式是只計算點下標 i<j 的那些對點之間的距離,這樣就避免了重復計算同一對點間距離。下面是蠻力法解決最近對問題的算法:
使用蠻力法求平面中距離最近的兩點 BruteForceClosetPoints(P) //輸入:一個n(n≥2)的點的列表P,Pi=(Xi,Yi) //輸出:距離最近的兩個點的下標index1和index2 dmin <— ∞ for i <— 1 to n-1 do for j <— i+1 to n do d <— sqrt( (Xi-Xi)2+(Yj-Yj)2 ) if d<dmin dmin=d; index1=i; index2=j; return index1,index2 |
該算法的關鍵步驟是基本操作雖然是計算兩個點之間的歐幾里得距離,但是求平方根並不是像加法乘法那么簡單。上面算法中,開平方函數導數恆大於0,它是嚴格遞增的,因此我們可以直接只計算(Xi-Xi)2+(Yj-Yj)2,比較d2的大小關系,這樣基本操作就變成了求平方。平方操作的執行次數是:
n(n-1) ∈ Θ(n2)
因此,蠻力法解決最近對問題的平均時間復雜度是Θ(n2)
下面是該算法的c++代碼實現部分,在實現這個算法時,我碰到了三個問題:
一是:怎么表示一個點集,因為最終返回的下標是集合中點的下標,要用的數據結構就是一維數組,但是點的xy坐標又要怎么表示呢,這里我在頭文件中創建了struct類型的點結構,該結構擁有的成員變量就是x代表的橫坐標和y代表的縱坐標,這樣就可以直接創建該結構的一位數組進行計算了。
二是:BruteForceClosetPoints(P)函數返回的是兩個最近對下標的值,但是用return只能返回一個值,因此這里在BruteForceClosetPoints函數的參數表中增加了引用類型的變量 &index1,&index2。
三是:在計算點間距離前,需要將最大值付給存儲當前最小距離的變量dmin,但是我不記得怎么表示float類型的最大值,搜索之后發現c++的<float.h>頭文件中定義了FLT_MAX這個常量表示float類型的最大值(其中也記錄了double類型的最值,而int類型是記錄在<limits.h>頭文件中)。
另外在實現時為更符合思路,我沒有使用數組的0號空間,而是直接從1號空間開始使用。
下面是源代碼部分:
//頭文件部分 #include<float.h>//該頭文件中存儲了float類型和double類型的最值 typedef struct Point{ float x; float y; }; void BruteForceClosetPoints(Point P[], int n, int &index1, int &index2); //源文件部分 #include <iostream> #include "最近對問題頭文件.h" using namespace std; int main(){ Point P[4]; for (int i = 1; i < 4; i++){ cin >> P[i].x >> P[i].y; } getchar(); /* cout << "您輸入的點的坐標為:" << endl; for (int i =1; i <4; i++){ cout<< "("<<P[i].x <<","<< P[i].y<<") "; } */ int index1, index2; BruteForceClosetPoints(P, 4, index1, index2); cout << "最近對的兩個點的下標為:" << index1 << " " << index2<< endl; getchar(); return 1; } void BruteForceClosetPoints(Point P[], int n, int &index1, int &index2){//由於要同時返回兩個點的下標,因此這里將形參設置為引用變量 float dmin = FLT_MAX;//常量FLT_MAX 來源於float.h頭文件 int i = 1,j=0; float d; for (; i < n-1; i++){ for (j = i + 1; j < n; j++){ d = (P[i].x - P[j].x)*(P[i].x - P[j].x) + (P[i].y - P[j].y)*(P[i].y - P[j].y); if (d < dmin){ dmin = d; index1 = i; index2 = j; } } } }
凸包問題
凸包問題是為一個有n個點的集合構造凸包的問題。凸包的定義是任意包含n>2個點(不共線的點)的集合s的凸包是以s中的某些點為頂點的凸多邊形(如果所有的點都位於一條直線上,多邊形退化為一條線段,但他的兩個端點仍然包含在s中)。對於凸多邊形,舉例子來說,三角形,長方形,直線都是,而五角星的形狀就是典型的非凸多邊形了。
用蠻力法解決凸包問題的思路之一是通過找到凸包的邊界(兩個極點組成的線段)來確定構成凸包的極點。
找到凸包邊界的算法是:對於一個n個點集合中的兩個點Pi和Pj,當且僅當該集合中的其他點都位於穿過這兩點的直線的同一邊是,他們的連線是該集合凸包邊界的一部分。
在實現上述算法的時候用到的解析幾何的基本知識是,當坐標平面上的兩個點(X1,Y1)、(X2,Y2)組成的直線方程是:ax+by=c(其中a=Y2-Y1,b=X1-X2,c=X1Y2-X2Y1)
這樣,這條直線將坐標平面分成分成兩個半平面,其中一個平面的點都滿足ax+by>c,另一個半平面的點都滿足ax+by<c,在直線上的點都滿足ax+by=c。因此,可以通過將點帶入ax+by-c這個解析式中判斷解析式值正負的方法來判斷某些點是否位於這條直線的同一邊。
蠻力法解決凸包問題: BruteForceConvexHull(P) //輸入:一個n個(n≥2)的點的列表P,Pi=(Xi,Yi) //輸出:能夠組成凸包的點的列表Qi=(Xi,Yi) for i <— 1 to n-1 do for j <— i+1 to n do sign1 <— 0;sign2 <— 0; a = yj - yi;b = xi - xj;c = xi*yj - yi*xj; for k <— 1 to n do if k=i||k=j continue if axk+byk-c≥0 sign1++; if axk+byk-c≤0 sign2--; if sign2=2-n || sign1=n-2 record the pole return OK |
上面的算法是我根據書上的描述自己寫的,在實現該算法的時候,遇到的問題就是不知道該怎么判斷除組成直線的兩個點外其余n-2個點都在該直線的同一側,后來網上搜索后發現有人設置了旗幟變量,於是我在他的基礎上做了改進,設置兩個旗幟變量,這樣可以處理當n-2個點中有點存在於該直線上這種情況。
該算法的基本操作就是最內層循環的if條件判斷,該語句執行的次數是:n(n-1)+n(n-2)+.....+n=n3/2 ∈ Θ(n3)
因此,概算法的平均時間復雜度是:n3
下面是該算法的c++實現代碼,在實現的過程中,依舊用struct結構來定義了點的數據結構,但是當判斷出一個點是極點后要將其坐標保存到新的數組中操作有點麻煩,於是我在struct結構中新增了singal屬性,用來存儲該點是否能夠構成凸包邊界的標志,這樣,在整個判斷結束后,只要重新遍歷一次原始列表P,將該列表中signal屬性被重新賦值的坐標輸出就是構成凸包的極點了。
//頭文件內容 #define NUM 7 //P列表點的個數 typedef struct Point{ float x; float y; int signal=0; }Point; int BruteForceConvexHull(Point P[], int n); //源文件內容 #include <iostream> #include "最近對問題頭文件.h" using namespace std; int main(){ Point P[NUM]; for (int i = 1; i < NUM; i++){ cin >> P[i].x >> P[i].y; } getchar(); BruteForceConvexHull(P, NUM-1); cout << "屬於凸包集合的點坐標為:" << endl; for (int i = 1; i <NUM; i++){ if (P[i].signal == 1){ cout << " (" << P[i].x << "," << P[i].y << ") "<<endl; } } getchar(); return 1; } int BruteForceConvexHull(Point P[], int n){ int i = 1, j = 1, k = 1; float a, b, c; int sign1 = 0, sign2 = 0; for (; i <= n - 1; i++){ for (j = i + 1; j <= n; j++){ a = P[j].y - P[i].y; b = P[i].x - P[j].x; c = P[i].x*P[j].y - P[i].y*P[j].x; sign1 = 0; sign2 = 0; for (k = 1; k <= n; k++){ if (k == i || k == j){ continue; } if ((a*P[k].x + b*P[k].y - c) <= 0){//此處必須是兩個if語句,避免當判斷的點在線上時只判斷了其中一個並且最好用兩個旗幟變量來記錄 sign1--; } if ((a*P[k].x + b*P[k].y - c) >= 0) { sign2++; } } if ((sign1 == 2 - n) || (sign2 == n - 2)){ //cout << sign1 << " " << sign2 << " 點 " << i << " " << j << " 是極點" << endl; P[i].signal = 1; P[j].signal = 1; } } } return 1; }