分治 | 最接近点对问题 —— 例题:套圈

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

 

本文目录

一、算法理论 

 1、分(Divide)

          2、治(Conquer)

 

二、算法实现

1、点的储存

2、排序的准备

3、求两点之间距离

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

完整代码:

 

三、具体题目

1、套圈

 

 



问题:给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、点的储存

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

#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;
}

 

三、具体题目

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秒 64M 0

这道题就是典型的最接近点对问题,只需要求出点集中的最短距离,然后除以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,做最扎实的程序员 ----

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

 

在这里插入图片描述

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