分治 | 最接近點對問題 —— 例題:套圈

耐心整理的精品筆記,花點時間耐心看完,相信會有收穫 ^_^ )

 

本文目錄

一、算法理論 

 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,做最紮實的程序員 ----

旨在用心寫好每一篇文章,平常會把筆記彙總成推送更新~

 

在這裏插入圖片描述

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