最近點對問題 分治法證明及兩種分治法

問題描述

對於平面上給定的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在右邊的點中向後查找,找到第一個不矮於iy座標d高度的點h(即hi點高度差小於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);
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章