使用分治法和蛮力法求解最近点对

  • 问题描述
    对于平面上给定的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)   //当startend相等时结束
    {
        while (start < end && compare(P[end], pivotKey, type))  //从后往前找到第一个笔pivotKey小的数
        {
            end--;
        }
        if (start < end) {  //填到start所指示的空位,start1,后移一位

            P[start++] = P[end];
        }

        while (start < end &&  compare(pivotKey, P[start], type)) //从前往后找到第一个笔pivotKey大的数
        {
            start++;
        }
        if (start < end) { //填到end所指示的空位,end1,前移一位
            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);
}
  1. 蛮力法

    假设输入规模为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;
}

运行结果:
这里写图片描述

猜你喜欢

转载自blog.csdn.net/weixin_36343850/article/details/78321844
今日推荐