递归与分治 | 2:最接近点对问题 —— 例题:套圈

耐心整理的精品笔记,花点时间耐心看完,相信会有收获 ^_^ )

目录

一、算法理论 

          1、分(Divide)

2、治(Conquer)

 

二、算法实现1(未改进版)

1、点的储存

2、排序的准备

3、求两点之间距离

4、分治法求点集最短距离

未改进的完整代码:

三、算法实现2(改进版)

🔺 改进:

改进后的完整代码:

四、具体题目

1、套圈

 

end 




问题:给n个点的坐标,求最近的一对点之间的距离。

  • 解法一:暴力穷举法,将所有点两两距离都算出,最终取最小的值。时间复杂度为O(n^2)
  • 解法二:分治法,下面具体说。时间复杂度为O(nlog(n)),理论上是最快的方法

(对解法二,本文给出了两种实现方式,未改进、改进代码都已经贴出。两种实现方式的思想是相同的,可以先看未改进的版本,再看看改进后版本的一些改变) 


一、算法理论 

 1、分(Divide)

对于最初输入的 n 个点构成的点集S,大致均分成两部分:以所有点的x坐标的中位数mid为界,分为点集S1:x坐标比mid小的点 和 点集S2:x坐标比mid大的点。如下图所示:

很明显,这里要继续分别向点集S1和S2递归地调用求解递归的终点:要处理的点集S只有两个点或者三个点,直接计算出最小距离返回。

(注:这里找中位数来分界是为了两个点集分的均匀,以规避算法性能最差的情况。尽管找到中位数也需要耗费一定时间,但是相比之下这会让算法更加高效)


2、治(Conquer)

对于点集S1已经求得最短距离d1,点集S2已经求得最短距离d2,两者的并集S的最短距离d是多少呢?暂且取 d = min(d1, d2)

🔺 分离出temp点集

需要注意到,在分界线两侧,容易出现比 d 更小的答案。经过论证(略,具体查阅算法书),我们需要在距离分界线 d 之内枚举各点,是否出现比 d 更小的答案,如果有,则要更新d。于是我们将在距离分界线 d 之内的各点储存在temp点集中,方便接下来的讨论。

🔺 在temp点集中找更小值

  1. 尽管temp点集已经缩小了范围,一一枚举还是有点浪费时间。那么还有更好的优化,我们将 temp点集 按照 y 坐标排序
  2. 然后对两两坐标一一枚举求距离。先确定某一点,然后一一枚举其后的其他点,求两点距离:先判断两点的 y 坐标是否超出d,如果超出,则不必再枚举,因为距离必将大于 d,其没有枚举完的点也是。(排序的作用就体现在此,方便枚举与舍弃)

下面这张图可以加深对枚举与舍弃的理解:

(注:可以验证(略,具体查阅算法书),每一个点枚举次数不会超过6个,所以不用担心枚举时的时间消耗和暴力做法一样)

 


二、算法实现1(未改进版)

1、点的储存

为了排序方便,使用结构体储存。

 

#define MAX_N 100005
struct node {
    double x;
    double y;
} point[MAX_N], temp[MAX_N];

 


2、排序的准备

中间要用到两种排序:

  1. 为了方便找到x的中位数,我们直接将对x排序好的点集传入函数。这里要对结构体内的x坐标排序。
  2. 中间为了方便舍弃,要对temp点集的y坐标排序。这里要对结构体内的y坐标排序。

为了方便且兼顾效率,对结构体数组排序,可以直接使用c++"algorithm"库中的sort函数,具体如何使用可以看看这篇文章:排序算法 | sort函数的使用。需要实现两种排序方式的cmp函数:

bool cmp_x(struct node p1, struct node p2) {
    return p1.x < p2.x;
}

bool cmp_y(struct node p1, struct node p2) {
    return p1.y < p2.y;
}

3、求两点之间距离

求两点之间的距离直接用公式哈,主要注意,平方处我们直接乘,不要调用pow函数,太慢了,小心超时。

double CalcDistance(struct node p1, struct node p2) {
    double dx = p1.x - p2.x;
    double dy = p1.y - p2.y;
    return sqrt(dx * dx + dy * dy);
}

4、分治法求点集最短距离

/* 查找点集中从下标start到end的点之间的最短距离
 * 注:点集已经按照x排好序 */
double MinDistance(int start, int end) {

    /* 递归的终点 */
    if (start + 1 == end)  //只有两个点
        return CalcDistance(point[start], point[end]);
    if (start + 2 == end) { //只有三个点
        double d1 = CalcDistance(point[start], point[start + 1]);
        double d2 = CalcDistance(point[start + 1], point[end]);
        double d3 = CalcDistance(point[start], point[end]);
        return min(d1, min(d2, d3));
    }

    /* 分 */
    int mid_index = (start + end) / 2;  // x中位数所在点的下标
    double d1 = MinDistance(start, mid_index);  //左边点集内的最小距离
    double d2 = MinDistance(mid_index + 1, end); //右边点集内的最小距离
    double d = min(d1, d2);

    /* 治 */
    int cnt = 0;  //记录temp点集点的个数
    for (int i = start; i <= end; i++)  //把x坐标在中界限[-d,d]附近的点一一收集到temp点集
        if (fabs(point[mid_index].x - point[i].x) <= d)
            temp[cnt++] = point[i];
    sort(temp, temp + cnt, cmp_y); //将temp点集按照y坐标排序
    for (int i = 0; i < cnt; i++)  //直接枚举,找出收集的点集里的最短距离
        for (int j = i + 1; j < cnt; j++) {
            if (temp[j].y - temp[j].y >= d)  //没有必要再找了,只会越来越大
                break;
            d = min(d, CalcDistance(temp[i], temp[j]));  //更新最小值
        }

    /* 返回分治的结果*/
    return d;
}

 


未改进的完整代码:

//
// Created by A on 2020/2/26.
//

#include <cstdio>
#include <cmath>
#include <algorithm>

using namespace std;

#define MAX_N 100005
struct node {
    double x;
    double y;
} point[MAX_N], temp[MAX_N];

bool cmp_x(struct node p1, struct node p2) {
    return p1.x < p2.x;
}

bool cmp_y(struct node p1, struct node p2) {
    return p1.y < p2.y;
}

double CalcDistance(struct node p1, struct node p2) {
    double dx = p1.x - p2.x;
    double dy = p1.y - p2.y;
    return sqrt(dx * dx + dy * dy);
}

/* 查找点集中从下标start到end的点之间的最短距离
 * 注:点集已经按照x排好序 */
double MinDistance(int start, int end) {

    /* 递归的终点 */
    if (start + 1 == end)  //只有两个点
        return CalcDistance(point[start], point[end]);
    if (start + 2 == end) { //只有三个点
        double d1 = CalcDistance(point[start], point[start + 1]);
        double d2 = CalcDistance(point[start + 1], point[end]);
        double d3 = CalcDistance(point[start], point[end]);
        return min(d1, min(d2, d3));
    }

    /* 分 */
    int mid_index = (start + end) / 2;  // x中位数所在点的下标
    double d1 = MinDistance(start, mid_index);  //左边点集内的最小距离
    double d2 = MinDistance(mid_index + 1, end); //右边点集内的最小距离
    double d = min(d1, d2);

    /* 治 */
    int cnt = 0;  //记录temp点集点的个数
    for (int i = start; i <= end; i++)  //把x坐标在中界限[-d,d]附近的点一一收集到temp点集
        if (fabs(point[mid_index].x - point[i].x) <= d)
            temp[cnt++] = point[i];
    sort(temp, temp + cnt, cmp_y); //将temp点集按照y坐标排序
    for (int i = 0; i < cnt; i++)  //直接枚举,找出收集的点集里的最短距离
        for (int j = i + 1; j < cnt; j++) {
            if (temp[j].y - temp[j].y >= d)  //没有必要再找了,只会越来越大
                break;
            d = min(d, CalcDistance(temp[i], temp[j]));  //更新最小值
        }

    /* 返回分治的结果*/
    return d;
}

 



三、算法实现2(改进版)

🔺 改进:

每次都为temp点集对y坐标进行排序比较消耗时间:

改进法1:可以考虑最开始传入的点集就是已经完全为y坐标排序好了的。由于之前为了方便找到中位数且将点集分到中位数两端,是直接将点集按照x坐标排好序才传入函数的,那么现在需要:将找中位数与将点集分到中位数两端的步骤具体写入。可以使用时间复杂度为O(n)的BFPTR算法,具体参考这篇文章递归与分治 | 1:选择算法/中位数 —— 例题:油井(实现比较复杂,理论上可以改进时间,暂未尝试)

改进法2:考虑将所有点分别存入两个点集(结构体数组)X、Y内第一个点集按照x坐标排序好,第二个点集按照y坐标排序好,并且Y点集中的每个点(用结构体/class实现)除了x、y坐标还要储存有对应该点在X点集的下标。一直在X点集内进行分治处理。

注意:在每个递归层中,X、Y、Temp 在 [l, r]内元素是一样的,就是排列方式不同(点集Y与点集Temp需要分离(递归前)和归并(递归后)操作)。然后在选出 "temp点集" 时能直接从Y点集中选取,并存入Temp点集中。因为传入的Y本身有序,就省去了对Y点集排序的时间。

(下面是改进法2的实现)


改进后的完整代码:

//
// Created by A on 2020/2/26.
//

#include <cstdio>
#include <cmath>
#include <algorithm>

using namespace std;

#define MAX_N 10005
typedef struct node {
    double x;
    double y;
    int index_x; //该点对应在X点集内的下标
} Node;
Node Point_X[MAX_N], Point_Y[MAX_N], Point_Temp[MAX_N];

bool cmp_x(Node p, Node q) {
    return p.x < q.x;
}

bool cmp_y(Node p, Node q) {
    return p.y < q.y;
}

double CalcDistance(Node p1, Node p2) {
    double dx = p1.x - p2.x;
    double dy = p1.y - p2.y;
    return sqrt(dx * dx + dy * dy);
}

/* 点集Y中的点对应X点集的下标按照:小于mid_index、等于mid_index、大于mid_index三部分
 * 顺序储存到Temp点集中
 * 最终:使得Temp[l,mid_index]与X[l,mid_index]、Temp[mid_index + 1,r]与X[l,mid_index]中元素是相同的
 * 并且,Temp[l,mid_index]与Temp[mid_index + 1,r]各自都已经是按照y坐标排好序的 */
void Divide(Node Y[], Node Temp[], int l, int mid_index, int r) {
    int p = l, q = mid_index + 1;
    for (int i = l; i <= r; i++) {
        if (Y[i].index_x > mid_index)
            Temp[q++] = Y[i];
        else
            Temp[p++] = Y[i];
    }
}

/* Temp[l,mid_index]与Temp[mid_index + 1,r]各自都已经是按照y坐标排好序的
 * 将两个序列合并到Y中,使得Y中点全都是按照y坐标排序好的*/
void Merge(Node Temp[], Node Y[], int l, int mid_index, int r) {
    int i = l, j = mid_index + 1;
    for (int t = l; t <= r; t++) {
        if (i > mid_index) {
            Y[t] = Temp[j++];
            continue;
        }
        if (j > r) {
            Y[t] = Temp[i++];
            continue;
        }
        if (Temp[i].y < Temp[j].y)
            Y[t] = Temp[i++];
        else
            Y[t] = Temp[j++];
    }
}

/* 计算点集X在[l,r]区间内的两点最近距离
 * 点集Y的元素与X内相同
 * 点集temp待用
 * 注:X在[l,r]区间已经按照x坐标排序,Y在[l,r]区间已经按照y坐标排序 */
double MinDistance(Node X[], Node Y[], Node Temp[], int l, int r) {
    /* 递归的终点 */
    if (l + 1 == r)  //只有两个点
        return CalcDistance(X[l], X[r]);
    if (l + 2 == r) { //只有三个点
        double d1 = CalcDistance(X[l], X[l + 1]);
        double d2 = CalcDistance(X[l + 1], X[r]);
        double d3 = CalcDistance(X[l], X[r]);
        return min(d1, min(d2, d3));
    }

    /* 分 */
    int mid_index = (l + r) / 2; // x中位数所在点的下标
    Divide(Y, Temp, l, mid_index, r);
    //注意Temp与Y传入的位置!!!!!
    double d1 = MinDistance(X, Temp, Y, l, mid_index); //左边点集内的最小距离
    double d2 = MinDistance(X, Temp, Y, mid_index + 1, r); //右边点集内的最小距离
    double d = min(d1, d2);

    /* 治 */
    int cnt = 0; //记录Temp点集点的个数
    Merge(Temp, Y, l, mid_index, r);   //还原Y点集(与该层参数刚传入时一样)
    for (int i = l; i <= r; i++) //把x坐标在中界限[-d,d]附近的点一一收集到Temp点集
        if (fabs(Y[i].x - X[mid_index].x) < d)
            Temp[cnt++] = Y[i];
    // 注:Temp点集内最终已经关于y坐标排序好
    for (int i = 0; i < cnt; i++) {  //直接枚举,找出收集的点集里的最短距离
        for (int j = i + 1; j < cnt; j++)
            if (CalcDistance(Temp[i], Temp[j]) > d) //没有必要再找了,只会越来越大
                break;
            else
                d = min(d, CalcDistance(Temp[i], Temp[j])); //更新最小值
    }

    /* 返回分治的结果*/
    return d;
}


int main() {
    int num = 0;
    scanf("%d", &num);
    for (int i = 0; i < num; i++)
        scanf("%lf %lf", &Point_X[i].x, &Point_X[i].y);
    sort(Point_X, Point_X + num, cmp_x);   //按照x坐标排序
    for(int i = 0; i < num; i++) {
        Point_Y[i] = Point_X[i];
        Point_Y[i].index_x = i;
    }
    sort(Point_Y, Point_Y + num, cmp_y);   //按照y坐标排

    printf("%.2lf", MinDistance(Point_X, Point_Y, Point_Temp, 0, num - 1));
}


四、具体题目

1、套圈

 

成绩10开启时间2020年02月25日 星期二 08:55
折扣0.8折扣时间2020年04月30日 星期四 23:55
允许迟交关闭时间2020年04月30日 星期四 23:55

Have you ever played quoit in a playground? Quoit is a game in which flat rings are pitched at some toys, with all the toys encircled awarded.
In the field of Cyberground, the position of each toy is fixed, and the ring is carefully designed so it can only encircle one toy at a time. On the other hand, to make the game look more attractive, the ring is designed to have the largest radius. Given a configuration of the field, you are supposed to find the radius of such a ring.
Assume that all the toys are points on a plane. A point is encircled by the ring if the distance between the point and the center of the ring is strictly less than the radius of the ring. If two toys are placed at the same point, the radius of the ring is considered to be 0.

Input The input consists of several test cases. For each case, the first line contains an integer N (2 <= N <= 100,000), the total number of toys in the field. Then N lines follow, each contains a pair of (x, y) which are the coordinates of a toy. The input is terminated by N = 0.

Output For each test case, print in one line the radius of the ring required by the Cyberground manager, accurate up to 2 decimal places.

 测试输入期待的输出时间限制内存限制额外进程
测试用例 1以文本方式显示
  1. 4↵
  2. 0 3↵
  3. 3 2↵
  4. 4 0↵
  5. 7 1↵
  6. 0↵
以文本方式显示
  1. 1.12↵
1秒64M0

这道题就是典型的最接近点对问题,只需要求出点集中的最短距离,然后除以2,即是题目所要求求的圈圈的半径。

注意点:

  1. 关于点的数据全用double也不知道为啥,之前x、y设置为int超时了
  2. 规范数据类型,一开始用float去计算double的距离,会导致精度丢失而wa
  3. 避免使用pow函数,太耗时了
  4. 放心使用库自带的快排和min函数,自己写容易超时

完整AC代码:

//
// Created by A on 2020/2/25.
//

#include <cstdio>
#include <cmath>
#include <algorithm>

using namespace std;

#define MAX_N 100005
struct node {
    double x;
    double y;
} point[MAX_N], temp[MAX_N];

bool cmp_x(struct node p1, struct node p2) {
    return p1.x < p2.x;
}

bool cmp_y(struct node p1, struct node p2) {
    return p1.y < p2.y;
}

double CalcDistance(struct node p1, struct node p2) {
    double dx = p1.x - p2.x;
    double dy = p1.y - p2.y;
    return sqrt(dx * dx + dy * dy);
}

/* 查找点集中从下标start到end的点之间的最短距离
 * 注:点集已经按照x排好序 */
double MinDistance(int start, int end) {

    /* 递归的终点 */
    if (start + 1 == end)  //只有两个点
        return CalcDistance(point[start], point[end]);
    if (start + 2 == end) { //只有三个点
        double d1 = CalcDistance(point[start], point[start + 1]);
        double d2 = CalcDistance(point[start + 1], point[end]);
        double d3 = CalcDistance(point[start], point[end]);
        return min(d1, min(d2, d3));
    }

    /* 分 */
    int mid_index = (start + end) / 2;  // x中位数所在点的下标
    double d1 = MinDistance(start, mid_index);  //左边点集内的最小距离
    double d2 = MinDistance(mid_index + 1, end); //右边点集内的最小距离
    double d = min(d1, d2);

    /* 治 */
    int cnt = 0;  //记录temp点集点的个数
    for (int i = start; i <= end; i++)  //把x坐标在中界限[-d,d]附近的点一一收集到temp点集
        if (fabs(point[mid_index].x - point[i].x) <= d)
            temp[cnt++] = point[i];
    sort(temp, temp + cnt, cmp_y); //将temp点集按照y坐标排序
    for (int i = 0; i < cnt; i++)  //直接枚举,找出收集的点集里的最短距离
        for (int j = i + 1; j < cnt; j++) {
            if (temp[j].y - temp[j].y >= d)  //没有必要再找了,只会越来越大
                break;
            d = min(d, CalcDistance(temp[i], temp[j]));  //更新最小值
        }

    /* 返回分治的结果*/
    return d;
}

int main() {
    int n;
    while (true) {
        /* 处理输入 */
        scanf("%d", &n);
        if (n == 0)
            break;
        for (int i = 0; i < n; i++)
            scanf("%lf %lf", &point[i].x, &point[i].y);

        sort(point, point + n, cmp_x);  //先将点按照x坐标排序
        printf("%.2lf\n", MinDistance(0, n - 1) / 2);
    }
}

结果:

效率有点低....尴尬。求大佬在评论区给出更快的改进呀...

(使用上面改正的改进代码会快很多,上面本题的完整代码是按照未改进的简单代码写的)



end 

欢迎关注个人公众号 鸡翅编程 ”,这里是认真且乖巧的码农一枚。

---- 做最乖巧的博客er,做最扎实的程序员 ----

旨在用心写好每一篇文章,平常会把笔记汇总成推送更新~

 

在这里插入图片描述

  • 11
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值