讲解分治法求最近点对问题的思想与算法实现。
利用分治法求最近点对与归并排序的结构上的相同,将时间复杂度降到真正意义上的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。
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
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
蛮力法的思想与算法实现:
遍历点集中的所有点,算出所有点与其他点的距离,比较得出点集中距离最短的两点的距离,并将两点记录下来。
时间复杂度: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得到真正距离,减少运算量
}
}