問題描述
對於平面上給定的N個點,給出所有點對的最短距離,即,輸入是平面上的N個點,輸出是N點中具有最短距離的兩點。
暴力法
暴力法的思路相對簡單,即枚舉所有可能的配對情況,然後一一比對,找到距離最小的點,一共有 Cn2 種組合,也就是 n*(n-1)/2種,總的時間複雜度是O(n^2),因爲時間開銷過於昂貴,我們尋求別的方法
分治法
問題分割策略:排序,取中位數
如果像快速排序,分割的策略與數據有關,那麼算法便會退化,不能達到穩定的O(nlogn)
但是如果效仿歸併排序,每次選取中點(中位數)進行分割,那麼可以使得遞歸樹的深度穩定的控制在log(n)
這裏對點做預處理,按照x升序,x相同則y升序排序,這樣我們每次選取中間下標的點,都能儘量地將點分割成幾何上的兩半
子問題存在分析:
問題是選取兩個點,p1,p2,使得他們距離最短,子問題存在於以下空間
- p1 p2 同時位於左邊點集
- p1 p2 同時位於右邊點集
- p1 p2 一個在左側一個在右側
對於子問題1和2,我們可以通過遞歸直接解決,難點就在於解決子問題3,即兩點在兩邊的情況
子問題3求解 縮小空間:
想起來歸併是要【合併】的,如何有效利用遞歸得到的答案呢?
注意這裏如果要一一配對找兩邊點求距離,那麼合併效率還是O(n^2)
遞歸得到兩邊點子集的最近點對距離,取最小值d,我們可以藉此距離d來縮小我們查找的空間
從中間點向外擴散,如果點與中間點的x左邊之差,大於d,那麼這個點及其往後的點,都不可能是最優的答案,縮小了查找空間
縮小查找空間,其實是不穩定的,可能查找空間和原來一樣,所以我們不能暴力枚舉查找空間內的點,下面引入一個結論,來幫助我們快速查找
結論:點目標出沒區域確定
結論:對於左邊區域的每一點p,要想在右邊區域找到一點p’使得 pp’距離<=d,這樣的點p’必定存在於【以p的y座標爲中心,上下延展d形成的d*2d的子矩形中】
證明:
假設極限情況,p在左右邊線上,以p爲中心半徑爲d畫圓,半圓區域內,都是距p的距離小於d的,半圓是矩形的真子集,故目標點一定存在於矩形中
而且因爲右側的點具有稀疏性,即兩兩之間距離大於等於d, d*2d
的矩形區域,矩形區域內最多存在6個點(如果圓形區域則是四個)
我們遍歷左邊區域的所有點,劃分出d*2d
的矩形區域,然後檢查6個點,更新答案即可,可是如何用短時間找到d*2d
的矩形區域? 想到矩形區域的劃分和y有關,那麼應該是利用y座標的特性了!
y的劃分
對左右的子區域的所有點按照y升序排序(左右分開排序,不是混合在一起),在指針i
升序遍歷左邊的所有點的時候,設置指針h
在右邊的點中向後查找,找到第一個不矮於i
點y
座標d
高度的點h
(即h
和i
點高度差小於d
),從h
往後找6個點即可
(因爲點y座標遞增,那麼對應的矩形區域,下邊界也遞增,那麼上一次找過的下界點(h指針),就不用再從頭開始找了,直接在上一次的基礎上找就行了)
合併代價O(n)的證明
因爲左右點集都是升序,這表明,i++之後,下一個i
點的d*2d
矩形的下邊界一定會提升,而h
指針隨着做出更新即可,因爲i點矩形的下邊界是遞增的,h指針不走回頭路
h指針最多從0增加到右邊區域點的個數,最多n/2次,所以遍歷左邊點集,h迭代的次數,均攤到每一次i迭代,就是均攤一次,O(1),而查找矩形區域總共是6個點,也是常數時間可以完成,所以總的複雜度仍然是O(n)
分治法的改進:
因爲每次遞歸之後,還要對左右按d劃分的區域排序,總複雜度是O(2*n/2log(n/2)), 即合併代價,不是線性效率,總體複雜度超過O(nlong(n))
那麼如何優化呢?
效仿歸併排序,每次二分遞歸之後,我們對數組歸併,因爲遞歸排好了左右子區間,這樣歸併後,數組就是對y有序的了,不用消耗額外的時間再排序了,可以使合併子問題的代價變爲O(n)級別
代碼
代碼的前半部分是一些幫助函數,後面兩個函數纔是分治法和歸併分治法的代碼
#include <bits/stdc++.h>
using namespace std;
typedef struct p
{
int x, y;
p(){}
p(int X, int Y):x(X),y(Y){}
}p;
/*
浮點最小函數,防止默認min的int形參截斷
*/
double lfmin(double a, double b)
{
return (a<b)?(a):(b);
}
/*
比較函數,排序用,x升序,x相同則y升序
param p1 : 第一個點
param p2 : 第二個點
return : p1 前於 p2 ?
*/
bool cmpx(const p& p1, const p& p2)
{
if(p1.x==p2.x) return p1.y<p2.y;
return p1.x<p2.x;
}
/*
比較函數,排序用,則y升序,歸併用
param p1 : 第一個點
param p2 : 第二個點
return : p1 前於 p2 ?
*/
bool cmpy(const p& p1, const p& p2)
{
return p1.y<p2.y;
}
/*
求解兩點歐氏距離
param p1 : 第一個點
param p2 : 第二個點
return : 距離,浮點數
*/
double dis(const p& p1, const p& p2)
{
return sqrt((double)(p1.x-p2.x)*(p1.x-p2.x)+(double)(p1.y-p2.y)*(p1.y-p2.y));
}
/*
求兩點水平距離
param p1 : 第一個點
param p2 : 第二個點
return : 水平距離,浮點數
*/
double disX(const p& p1, const p& p2)
{
double ans = (double)p1.x - (double)p2.x;
if(ans<0) return ans*-1;
return ans;
}
/*
暴力求解最近點對
param points : 點的數組
return : 最近點對距離
*/
double cp(vector<p>& points)
{
double ans = (double)INT_MAX;
for(int i=0; i<points.size(); i++)
for(int j=i+1; j<points.size(); j++)
ans = lfmin(ans, dis(points[i], points[j]));
return ans;
}
/*
分治求解最近點對,複雜度O(nlog(n)log(n))
param points : 點的數組
param l : 點數組左端點
param r : 點數組右端點
return : 最近點對距離
explain : 區間[l, r]左閉右閉
*/
double cp(vector<p>& points, int l, int r)
{
if(l>=r) return (double)INT_MAX;
if(l+1==r) return dis(points[l], points[r]);
int mid=(l+r)/2, le=mid, ri=mid, h=0;
double d=lfmin(cp(points, l, mid), cp(points, mid+1, r)), ans=d;
vector<p> ll, rr;
while(le>=l && disX(points[mid], points[le])<=d) ll.push_back(points[le]), le--;
while(ri<=r && disX(points[mid], points[ri])<=d) rr.push_back(points[ri]), ri++;
sort(ll.begin(), ll.end(), cmpy), sort(rr.begin(), rr.end(), cmpy);
for(int i=0; i<ll.size(); i++)
{
while(h<rr.size() && rr[h].y+d<ll[i].y) h++; h=min((int)rr.size(), h);
for(int j=h; j<min((int)rr.size(), h+6); j++)
if(!eqfunc()(ll[i], rr[j])) ans=lfmin(ans, dis(ll[i], rr[j]));
}
return lfmin(ans, d);
}
/*
分治+歸併求解最近點對,複雜度O(nlog(n))
param points : 點的數組
param l : 點數組左端點
param r : 點數組右端點
return : 最近點對距離
explain : 區間[l, r]左閉右閉
*/
double cpm(vector<p>& points, int l, int r)
{
if(l>=r) return (double)INT_MAX;
if(l+1==r)
{
if(cmpy(points[r], points[l])) swap(points[l], points[r]);
return dis(points[l], points[r]);
}
int mid=(l+r)/2, le=mid, ri=mid, h=0;
p midp = points[mid];
double d=lfmin(cpm(points, l, mid), cpm(points, mid+1, r)), ans=d;
inplace_merge(points.begin()+l, points.begin()+mid+1, points.begin()+r+1, cmpy);
vector<p> ll, rr;
for(int i=l; i<=r; i++)
{
if(midp.x>=points[i].x && disX(midp, points[i])<=d) ll.push_back(points[i]);
if(midp.x<=points[i].x && disX(midp, points[i])<=d) rr.push_back(points[i]);
}
for(int i=0; i<ll.size(); i++)
{
while(h<rr.size() && rr[h].y+d<ll[i].y) h++; h=min((int)rr.size(), h);
for(int j=h; j<min((int)rr.size(), h+6); j++)
if(!eqfunc()(ll[i], rr[j])) ans=lfmin(ans, dis(ll[i], rr[j]));
}
return lfmin(ans, d);
}