分治法与蛮力法求最近点对问题(分治法时间复杂度O(nlogn))

讲解分治法求最近点对问题的思想与算法实现。

利用分治法求最近点对与归并排序的结构上的相同,将时间复杂度降到真正意义上的O(nlogn)而不是O(nlognlogn)。

 

1. 预处理:创建结构体Node附带属性x,y表示在平面坐标系中的位置,由Node构成点集S,任一Node就是点集S中的点,生成点时要保证任意两点不重复。

2. 点数较少时的情形(点数为1返回无穷大,点数为2直接计算返回距离,本实现方法不允许在三个点时也直接计算,两个点以上时必须再均分,这样才符合归并排序的二分法)。

 

3. 点数|S|>=3时,将平面点集S分割成为大小大致相等的两个子集SL和SR,选取一个垂直线L作为分割直线。通过先对点集以x进行预排序,然后取下标的中间值,就能将数组分为均等的两半( mid=(l+r)/2 ),在不将预排序的时间计入的情况下,此分割方法在单个递归函数中的时间复杂度在为O(1)。

 

4. 两个递归调用,分别求出SL和SR中的最短距离为dl和dr。递归调用的实际过程就是再把SL和SR进行划分成左右两部分,再对左右两部分继续进行划分,直至划分的部分只包含一个或两个点才停止,并开始计算返回。

5. 取d=min(dl, dr),在直线L两边分别扩展d,得到中间区域,将中间区域的点放入一个新点集中并按y进行排序(我的实现方法是先对所有点进行Y轴排序然后再存入一个临时的中间点集数组中),对y排序使得对于中间区域的任意一点,我们都可以快速找到在y轴方向上离它最近的点。

 

6.不能直接在递归函数中写一个排序方法对y进行排序,因为二分法递归本身的复杂度为logn,而排序算法最快为nlogn,这样子总的时间复杂度就会变成nlognlogn。利用归并排序和最近点对两者在算法结构上的相同(二分法),在递归函数中一边求子数组的最近点对距离,一边利用归并排序中的合并操作(在单个递归函数中的复杂度为O(n))按y对点集进行整理排序,总时间复杂度就为nlogn。

 

这一步会比较难理解,如上图:假设原数据按x排好序,方块中显示的是它们y轴上的值,初始点数组的y轴数据为【5,1,2,4,3,7,6】;分治法最后会将所有点分成只有1个或2个的情况,如【5】,【1,2】,【4,3】,【7,6】 ,在分到一个点的情况,函数会返回这个点(只有一个点所以一定是有序的)和一段无穷大的距离,在两个点的情况,函数通过计算和merge合并操作会返回一个有序的子序列和最近的两个点间的距离,比如【4,3】会返回dis(点4,点3)和【3,4】,【6,7】会返回dis(点6,点7)和【6,7】,接下来就merge合并【3,4】,【6,7】得到【3,4,6,7】,同时比较dis(点4,点3)和dis(点6,点7)得到较短的距离。用过这种方法不断在返回最近点对距离的同时返回一个对y有序的数组,直到统计完所有的点。 

7.对于中间区域SL的每一点,检查SR中的点与它的距离,更新所获得的最近距离。为使此步骤做到线性效率,我选择将SL和SR合并起来。我们将已对y排好序且均位于中间区域的点的数组从第一个点开始向后遍历,检测两点间的距离(因为每个点已经与前面的点比较过,所以只需向后遍历)。因为左右两边的已找到的最近点对距离为mindis,若中间区域的某点A存在另一点构成距离更短的点对,这两点的y值之差必须小于mindis。画出长为2*mindis,宽为mindis的矩形,点A位于矩形上边,所有与A有可能构成最近点对的候选点应位于矩形候选区域中。SL和SR中的矩形(正方形)各自最多只能存在4个点(位于顶点),但点集中不存在重复的点,所以一旦有一个矩形中有两个点占据了中线的上顶点,另一矩形便最多只能在中线上存在一个点,所以整个矩形中最多存在7个点。但因为点A位于矩形边上,在图1情况下,左边矩形上边的两个顶点都不可能存在,所以最多为5个点,在图2情况下(此时中线上的点A算左边区域),被点A覆盖的点不可能存在,所以最多为6个点。所以对于任意一点,最多只需向下遍历6个点就行,所以此操作是线性效率。

8. 在计算过程中,每次获得最短距离时,要记录下点对的位置信息。

分治法的递归操作每次都是均等分,所以其递归树为logn层,递归函数中的操作均为线性,所以总的时间复杂度为O(nlogn)。

T(n) = 2T(n/2)+O(n)递推= O(nlogn)

分治法实现方法:

//定义储存点的结构
struct Node {	
	double x;
	double y;
};

Node *PointsX;
Node *PointsY;

int main(){
	//省略
	PointsX = new Node[pointNumber];
	CreatePoints(PointsX, pointNumber);			//创建散点	
	sort(PointsX, PointsX + pointNumber, cmpX);		//对点以X坐标进行排序
	PointsY = new Node[pointNumber];		//创建新数组在Merge合并操作时用
	dis = MergeMethod(PointsX, 0, pointNumber - 1, minPoint1, minPoint2);	//调用分治法
	//省略
	return 0;
}
//随机生成点
void CreatePoints(Node Points[], int pointNumber){
	srand(time(NULL));		//随机化随机数种子
	for (int i = 0; i<pointNumber; i++){
		Points[i].x = rand()/(double)RAND_MAX;
		Points[i].y = rand()/(double)RAND_MAX;
		//每1000个数据乘以一个特定的数,将数据尽量分散,减少重复
		Points[i].x *= ((i / 1000) + 1);
		Points[i].y *= ((i / 1000) + 1);
		//遍历已经生成的所有点,一旦发现重合则重新生成该点
		for (int j = 0; j < i; j++){
			if (Points[j].x == Points[i].x && Points[j].y == Points[i].y) {
				i--;
				break;
			}
		}
	}
}

 

double MergeMethod(Node *px, int l, int r, Node &p1, Node &p2) {
	if (r - l <= 0)	{//点数为1时输出无穷大
		return MAX_DISTANCE;
	}
	else if (r - l == 1) {		//点数为2直接输出点距同时记录点对
		Merge(l,r);
		p1 = px[l];
		p2 = px[r];
		return sqrt((px[r].x - px[l].x)*(px[r].x - px[l].x) + (px[r].y - px[l].y)*(px[r].y - px[l].y));
	}
	else {
		Node c, d, e, f;
		double mindis;
		int i, j;
		int mid = (r + l) / 2;		//前面已排好序,直接平均分
		
		double middlex = PointsX[mid].x;	//记录中线的x值,用于后面判断和存储中间区域的点

		double mindis1 = MergeMethod(px, l, mid, c, d);		//求左边部分的最短点距
		double mindis2 = MergeMethod(px, mid+1, r, e, f);	//求右边部分的最短点距

		if (mindis1 < mindis2){		//两边比较取最小值,并记录点对
			mindis = mindis1;
			p1 = c;
			p2 = d;
		}
		else{
			mindis = mindis2;
			p1 = e;
			p2 = f;
		}

		Node *temp = new Node[r - l + 1];		//将所有与中间点的横坐标距离小于mindis的点纳入数组
		int number = 0;
		
		Merge(l, r);			//对点进行合并操作,之后的数组已是按y值排好的数组

		for (i = l; i <= r; i++) {
			if (fabs(px[i].x - middlex ) <= mindis) {	//数组内的数据相当于在横坐标范围[middlex-mindis,middlex+mindis]之间
				temp[number++] = px[i];
			}
		}

		double tempdis;		//遍历中间数组,每个点最多遍历其他点6次,记录最短距离和点对
		for (i = 0; i < number; i++) {
			for (j = i + 1; j < i+1+6 && j < number; j++) {
				tempdis = sqrt((temp[i].x - temp[j].x)*(temp[i].x - temp[j].x) + (temp[i].y - temp[j].y)*(temp[i].y - temp[j].y));
				if (tempdis < mindis) {
					mindis = tempdis;
					p1 = temp[i];
					p2 = temp[j];
				}
			}
		}
		delete[]temp;
		return mindis;
	}
}

bool cmpX(Node &a, Node &b){
	return a.x<b.x;
}
//归并排序的合并整理操作
void Merge(int left, int right) {

	int mid = (left + right) / 2;
	int i = left, j = mid + 1;
	int idx = left;

	while (i <= mid && j <= right) {
		if (PointsX[i].y < PointsX[j].y) 
			PointsY[idx++] = PointsX[i++];
		else 
			PointsY[idx++] = PointsX[j++];
	}

	while (i <= mid) 
		PointsY[idx++] = PointsX[i++];
	while (j <= right) 
		PointsY[idx++] = PointsX[j++];

	for (i = left; i <= right; i++) 
		PointsX[i] = PointsY[i];
}

蛮力法的思想与算法实现:

遍历点集中的所有点,算出所有点与其他点的距离,比较得出点集中距离最短的两点的距离,并将两点记录下来。

时间复杂度:T(n) =an² + bn + c

蛮力法实现方法:

 

//蛮力法
double BruteForceMethod(Node p[], int length)
{
	if (length == 1)		//点数为1输出无穷大
		return MAX_DISTANCE;
	else if (length == 2)	//点数为2直接输出点距
		return ((p[0].x - p[1].x)*(p[0].x - p[1].x) + (p[0].y - p[1].y)*(p[0].y - p[1].y));
	else{
		int i, j;
		double mindis, temp;
		mindis = (p[0].x - p[1].x)*(p[0].x - p[1].x) + (p[0].y - p[1].y)*(p[0].y - p[1].y);	//先取前两个点的点距为最小距离
		minPoint1 = p[0];
		minPoint2 = p[1];
		for (i = 0; i < length - 1; i++){
			for (j = i + 1; j < length; j++){
				temp = (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 (temp < mindis){				{
					mindis = temp;			//当发现更小的距离时,替换最小点距,并记录两个点的信息
					minPoint1 = p[i];
					minPoint2 = p[j];
				}
			}
		}
		return mindis;			//直到返回结果时才求sqrt得到真正距离,减少运算量
	}
}

 

 

 

 

  • 74
    点赞
  • 271
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值