使用分治法和蠻力法求解最近點對
問題描述
對於平面上給定的N個點,給出所有點對的最短距離,即輸入是平面上的N個點,輸出是N點中具有最短距離的兩點。求解
建立點類,使之具有兩個屬性,x座標和y座標
class myPoint {
public:
int x; //x座標
int y; //y座標
};
比較函式
bool compare(myPoint a, myPoint b, int type) { //若type=1,表示比較x座標;若type=2,表示比較y座標。
if (type == 1) {
return a.x > b.x;
}
else {
return a.y > b.y;
}
}
隨機生成點集,放在X集和Y集中。注意:X集和Y集中點的內容是一樣的,只是排序的方法不同而已,X集中的點按照x座標進行排序,Y集中的點按照y座標進行排序
void Initial(int N, myPoint* X, myPoint* Y) {
srand(N); //種子為資料的規模
int i;
for (i = 0; i < N; i++) { //產生隨機數,X集和Y集中點的內容是一樣的,只是排序的方法不同而已
X[i].x = rand();
X[i].y = rand ();
Y[i].x = X[i].x;
Y[i].y = X[i].y;
}
}
距離函式
double Distance(myPoint p1, myPoint p2) { //計算兩個點的距離
return sqrt((p1.x - p2.x)*(p1.x - p2.x) + (p1.y - p2.y)*(p1.y - p2.y));
}
快排演算法(使用分治法前,必須先排序)
void quickSort(myPoint* P, int start, int end, int type) { //若type=1,表示比較x座標;若type=2 ,表示比較y座標
if (start == end) //只有一個點時,直接返回
return;
int primary_start = start; //儲存初始的第一個首位置和尾位置
int primart_end = end;
myPoint pivotKey = P[start]; //樞軸量預設為第一個數
while (start < end) //當start和end相等時結束
{
while (start < end && compare(P[end], pivotKey, type)) //從後往前找到第一個筆pivotKey小的數
{
end--;
}
if (start < end) { //填到start所指示的空位,start增1,後移一位
P[start++] = P[end];
}
while (start < end && compare(pivotKey, P[start], type)) //從前往後找到第一個筆pivotKey大的數
{
start++;
}
if (start < end) { //填到end所指示的空位,end減1,前移一位
P[end--] = P[start];
}
}
P[start] = pivotKey; //此時end==start,把pivotKey的值填到中間的空位
if (start - 1>primary_start) //遞迴前面一半進行快排
quickSort(P, primary_start, start - 1, type);
if (start + 1<primart_end)//遞迴後面一半進行快排
quickSort(P, start + 1, primart_end, type);
}
蠻力法
假設輸入規模為n,則應該求出每個點與其他n-1個點之間的距離,並將它與最小距離做比較,若小於最小距離,則更新最小距離。則總共應該執行操作n(n-1)次,故演算法的複雜度為O(n2)。
double force(int start, int end, myPoint* P, myPoint& P1, myPoint& P2) { //暴力法
int i, j;
if (end - start<1) { //只有一個點時,直接返回
return 0.0;
}
double minDis = Distance(P[start], P[start + 1]); //初始化
P1 = P[start];
P2 = P[start+1];
for (i = start; i <= end; i++) {
for (j = start; j <= end; j++) {
if (i != j && Distance(P[i], P[j])<minDis) {
minDis = Distance(P[i], P[j]);
P1 = P[i];
P2 = P[j];
}
}
}
return minDis;
}
2.分治法
(1) 預排序:
將點集存放到陣列X中,陣列X中的點集按照x座標進行排序;將點集存放到陣列Y中,陣列Y中的點集按照y座標進行排序。陣列X和陣列Y中總體的內容是相同的,都包含整個點集的X座標和Y座標,只是排序不一樣而已。排序過程呼叫快速排序演算法,其演算法複雜度是O(n logn)
(2) 判斷:
在分治演算法中,若判斷到點集的數量小於等於3,則直接呼叫蠻力法進行排序;若點集格式大於3,則採用分治的思想來求解。
(3) 分解:
找出一條垂直線L,將點集按照x座標的順序的不同分成兩部分。由於之前做了預排序,所以取陣列的中位數,就可以直接點集近乎平分成兩半。
(4) 解決:
將上一步分成兩半的子點集,遞迴呼叫分治演算法對問題規模進行分解。當遞迴分解到符合點集的數目小於等於3後,通過蠻力法求解最小距離並且組成返回。此時可以得到左邊的最小距離minL,右邊的最小距離minR。選取min(minL,minR)作為當前的最小的距離s。
(5) 合併:
判斷左、右兩部分子點集中,是不是存在一個點a在左邊的子點集,另外一個點b在右邊的子點集,使得a,b之間的距離小於之前求得的最小距離s。那麼左邊的n/2 個點分別與右邊的n/2個點進行比較,就要比較n2/4次,使得演算法複雜度為O(n2),這使得與蠻力法沒多大差別,我們尋找更好的解決方法。我們嘗試縮小搜尋的範圍,由於當前的最小距離是s,所以在距離中間線的寬度為s之外的點,不可能從對邊找到一個距離小於s的點。接著我們繼續縮小搜尋的範圍,對於一個點p,若要找到一個與它的距離不大於s的點q,則q的縱座標與p的縱座標的差值必須不大於s。所以我們得到了一個可能的滿足條件的矩形區域。
如上圖所示,若點p為圖中的粉紅色小點,且點a可能的存在範圍為藍線區域。則綠色的區域所圍成的邊長為2S的矩形為q點可能的存在範圍。由互相排斥的性質可得,極端情況下,這個區域裡面最多總共有12個點,圖中的每個紫色的位置最多有一個點,總共6個點;圖中的黃色點位置,每個位置可能有兩個點,一個屬於左邊,一個屬於右邊,總共6個點。所以加起來總共有12個點。現在,我們一開始對y座標進行排序的陣列Y可以派上用場了。遍歷陣列Y,若某個點落在到中線距離小於s的長條形區域上,也就是圖中的兩條紅線所夾區域,則加入到新陣列C中。所以,在合併的過程中,複雜度為O(n)。有因為迭代了logn層遞迴呼叫,所以分治過程演算法複雜度為O(nlogn)。又因為預排序的演算法複雜度為O(nlogn),所以整個分治演算法的複雜度依舊為O(nlogn)
我們做一條直線橫切綠色矩形,將綠色矩形分割為兩半。直線上面的點即屬於上半矩形又屬於下半矩形,所以每半個綠矩形現在最多有八個點。我們遍歷陣列C中的所有點,判斷每個點後面的7個點是否與自身的距離小於s,若小於s則更新s。只判斷後面7個點而不判斷前面的點是因為當使有的點都判斷後面7個點就足夠複雜所有情況了。
double divide_conquer(int start, int end, myPoint* X, myPoint* Y, myPoint& P1, myPoint& P2) {
if (end - start < 3) //當點的數量小於等於3時,用暴力法。由於是按照x座標來劃分區域,固X陣列的點的數量和位置君未發生改變,故用X陣列不用Y陣列
return force(start, end, X,P1,P2);
int mid = (start + end) / 2; //按照x座標的排序,從中間切開
int leftLen = 0, rightLen = 0, i, j; //leftLen,和rightLen分別指示左右兩邊點集的個數
myPoint* YL = new myPoint[(end - start + 1) ]; //儲存中線左邊的點集,按照Y座標排序
myPoint* YR = new myPoint[(end - start + 1) ]; //儲存中線右邊的點集,按照Y座標排序
for (i = 0; i <= end - start; i++) { //遍歷集合中每一個點,他們都是按照y的順序進行排序的,若點的x座標小於等於中線,則分割到左子集,否則分割到右子集
//通過此操作YL和YR中的點的座標依舊是按照y座標排序好的
if (Y[i].x <= X[mid].x) {
YL[leftLen++] = Y[i];
}
else {
YR[rightLen++] = Y[i];
}
}
double left = divide_conquer(start, mid, X, YL,P1,P2); //遞迴求左邊部分的最短距離
myPoint leftP1 = P1; //儲存左支距離最小的兩個點的座標
myPoint leftP2 = P2;
double right = divide_conquer(mid + 1, end, X, YR,P1,P2); //遞迴求右邊部分的最短距離
double minDis; //取小的一個
if (left < right) {
minDis = left;
P1 = leftP1;
P2 = leftP2;
}
else { //此時並不需要更新P1,P2的值,因為此時P1和P2的值就是右支的最小的一對點的座標
minDis = right;
}
myPoint* newY = new myPoint[(end - start + 1)]; //新建一個數組,用於儲存以中界線為中心,寬度為 2*minDis 的垂直帶形區域內的點
int newYLen = 0;
double leftBorder = X[mid].x - minDis; //區域的左邊界
double rightBorder = X[mid].x + minDis; //區域的右邊界
for (i = 0; i <= end - start; i++) { //遍歷Y集中所有點,滿足條件則加入新集合,新集合的點的依然按照y座標排序好
if (Y[i].x >= leftBorder && Y[i].x <= rightBorder)
newY[newYLen++] = Y[i];
}
for (i = 0; i<newYLen; i++) { //在新集合中,判斷每個點跟它後面的7個點的距離作比較,若小於則更新最小距離
for (j = 1; j <= 7; j++) {
if ((i + j)<newYLen) { //加入條件,防止越界
double dis = Distance(newY[i], newY[i + j]);
if (dis < minDis) {
minDis = dis;
P1 = newY[i];
P2 = newY[i + j];
}
}
}
}
delete YL;
delete YR;
delete newY;
return minDis;
}
void Initial(int N, myPoint* X, myPoint* Y) {
srand(N); //種子為資料的規模
int i;
for (i = 0; i < N; i++) { //產生隨機數,X集和Y集中點的內容是一樣的,只是排序的方法不同而已
X[i].x = rand();
X[i].y = rand();
Y[i].x = X[i].x;
Y[i].y = X[i].y;
}
}
值得注意的是,在對中間區域的點按照y座標進行排序的過程中,必須充分利用之前有序的陣列Y,在O(n)的複雜度下產生新的陣列。之前在網上參考了相關的過程,發現有一些演算法就沒有解決好這麼一個問題。他們對陣列Y中抽取出來的點進行快速排序,那麼合併過程的複雜度就變成O(nlogn),再加上logn 層遞迴呼叫,使得演算法的總體複雜度變成了O(n(logn)2),並不能達到我們想要達到的O(nlogn)的複雜度。
最後給出main函式和執行結果
#include<iostream>
#include<math.h>
#include<stdlib.h>
#include<time.h>
const int Count = 100; //重複計算的次數,解決小規模時時間太短無法得出時間的問題
int main()
{
int N;
cout << "請輸入問題的規模:";
cin >> N;
myPoint* X = new myPoint[N]; //X,Y兩個點集的內容相同,區別在於X點集是按照x座標順序排序,而Y點集市安裝y座標順序排序
myPoint* Y = new myPoint[N];
int i;
clock_t start, end;
double ave = 0.0;
double minDis = 0.0;
myPoint P1, P2; //距離最小的兩個點的座標
Initial(N, X, Y); //初始化陣列
for (i = 0; i < Count; i++) { //重複count次
start = clock();
minDis += force(0, N - 1, X,P1,P2);
end = clock();
ave += (double)(end - start);
}
ave /= Count;
minDis /= Count;
cout << "暴力演算法: 最短距離為:" << minDis << " 時間為:" <<ave<<" ms"<< endl;
cout << "最小距離的兩個點的座標為:(" << P1.x << "," << P1.y << "),(" << P2.x << "," << P2.y << ")"<<endl;
ave = 0.0;
minDis = 0.0;
for (i = 0; i < Count; i++) { //重複count次
start = clock();
quickSort(X, 0, N - 1, 1); //將點集按照x座標的升序排序,並儲存到X陣列中
quickSort(Y, 0, N - 1, 2); //將點集按照y座標的升序排序,並儲存到Y陣列中
minDis+= divide_conquer(0, N - 1, X, Y,P1, P2);
end = clock();
Initial(N, X, Y); //重新初始化,供下一次使用
ave += (double)(end - start);
}
ave /= Count;
minDis /= Count;
cout << "分治演算法: 最短距離為:" <<minDis<< " 時間為:" << ave << " ms" << endl;
cout << "最小距離的兩個點的座標為:(" << P1.x << "," << P1.y << "),(" << P2.x << "," << P2.y << ")" << endl;
delete[]X;
delete[]Y;
return 0;
}
執行結果: