---
# System prepended metadata

title: 最近點對問題
tags: [mycourse]

---

## 最近點對問題 Closest Pair of Points
![最近點](https://hackmd.io/_uploads/rJObdgJXge.png)
reference: APCS進階程式設計課程 課本 ==6-2 分⽽治之==

---
緣起:
> 有位同學問了，
> 老師，為什麼我寫了時間 ==複雜度較低== 的程式，但 APCS 跑出來的 ==時間== 卻更久
> :star: 感謝 `20803 江旻達` 同學 2024/06 提供的 [非常好問題](https://zh.wikipedia.org/zh-tw/早上好中国，现在我有冰淇淋#:~:text=激情9，因為-,非常好電影,-，動作非常好) 與兩個不同方法的 `程式碼` :star:

[TOC]
### 題目
10000 個二維座標點 $(0 \le x, y \le 40000)$ 找最近的兩個點的 ==距離==

### 簡述策略
首先這題分而治之的大方向是:
```cpp=
// 1. 從 x 座標分割成左右兩個半邊
mid = (left + right) / 2;

// 2. 取得兩半邊各自的最近點對 "距離" 後
min_left  = get_min_distance(arr, left, mid);
min_right = get_min_distance(arr, mid, right);

// 3. 針對交錯的點對計算最短距離 (一個點在左，一個點在右的 pair)
// 3.1 根據推論(請查看課本)，發現一個好特性，
// 只需要抓分割線左右邊 - 距離 d 的點來判斷就好了
d = min(min_left, min_right);

// 3.2 找交錯 pair 的最小距離
// 在 X 座標於 [mid - d, mid + d] 這個區間中的點集中起來
// 找最小距離
```

他寫的兩個版本（如同課本介紹的順序）分別是
1. 將中間的所有點兩兩比較 $O(n^2)$
2. 將中間的所有點 沿著 y 軸排序 $O(n \log n)$，再從上到下檢查：每個點，與後七個點的距離$O(n)$

:::spoiler 1.cpp
```cpp=

// old method
#include <iostream>
#include<cmath>
#include<algorithm>
#include<iomanip>
#include <chrono>
using namespace std;
struct P{
    double x; double y;
};
bool cmpx(P a,P b){
    return a.x<b.x;
}
bool cmpy(P a,P b){
    return a.y<b.y;
}
double d(P a,P b){
    return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
double closestpair(P arr[],int h,int l){
    int n=h-l+1;
    if(n==1)return(-1);
    if(n==2)return(d(arr[l],arr[h]));
    if(n==3)return(min(d(arr[l],arr[h]),min(d(arr[l+1],arr[h]),d(arr[l],arr[l+1]))));

    int mid=(h+l)/2;
    double dl=closestpair(arr,mid,l);
    double dr=closestpair(arr,h,mid+1);
    double ans=min(dl,dr);

    int midl=mid;
    int midh=mid;
    int midx=arr[mid].x;
    for(int i=mid;i>=l;i--){
        if(arr[i].x<midx-ans) break;
        midl=i;
    }
    for(int i=mid;i<=h;i++){
        if(arr[i].x>midx+ans) break;
        midh=i;
    }
    for(int i=midl;i<=midh;i++){
        for(int j=i+1;j<=midh;j++){
            ans=min(ans,d(arr[i],arr[j]));
        }
    }
    // int k=0;
    // P arry[n];
    // for(int i=l;i<=h;i++){
    //     if(arr[i].x>=arr[mid].x-ans&&arr[i].x<=arr[mid].x+ans){
    //         arry[k].x=arr[i].x;
    //         arry[k].y=arr[i].y; 
    //         k++;
    //     }  
    // }
    // sort(arry,arry+k,cmpy);
    // for(int i=0;i<k;i++){
    //     for(int j=0;j<7;j++){
    //         if(i+j+1<k)ans=min(ans,d(arry[i],arry[i+j+1]));
    //     }
    // }
    // k=0;
    return(ans);
}
int main(){
    auto start = std::chrono::high_resolution_clock::now();

    P arr[10001];
    int N; 
    while(cin>>N){
        if(N==0) break;
        else{
            for(int i=0;i<N;i++){
                cin>>arr[i].x>>arr[i].y;
            }
        }
        sort(arr,arr+N,cmpx);
        double d=closestpair(arr,N-1,0);
        if(d>10000||d==-1){
            cout<<"INFINITY "<<endl;
        }
        //else cout << setiosflags (ios::fixed) << setprecision (4)<< floor (d * 10000)/10000<< endl;
        else cout<<fixed<<setprecision(4)<<d<<endl;
    }
    auto end = std::chrono::high_resolution_clock::now();

    // 計算時間差
    std::chrono::duration<double> elapsed = end - start;

    // 顯示經過的時間（秒）
    std::cout << "Elapsed time: " << elapsed.count() << " seconds\n";
}
```
:::

:::spoiler 2.cpp
```cpp=

// new method
#include <iostream>
#include<cmath>
#include<algorithm>
#include<iomanip>
#include <chrono>
using namespace std;
struct P{
    double x; double y;
};
bool cmpx(P a,P b){
    return a.x<b.x;
}
bool cmpy(P a,P b){
    return a.y<b.y;
}
double d(P a,P b){
    return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
double closestpair(P arr[],int h,int l){
    int n=h-l+1;
    if(n==1)return(-1);
    if(n==2)return(d(arr[l],arr[h]));
    if(n==3)return(min(d(arr[l],arr[h]),min(d(arr[l+1],arr[h]),d(arr[l],arr[l+1]))));

    int mid=(h+l)/2;
    double dl=closestpair(arr,mid,l);
    double dr=closestpair(arr,h,mid+1);
    double ans=min(dl,dr);

    // int midl=mid;
    // int midh=mid;
    // int midx=arr[mid].x;
    // for(int i=mid;i>=l;i--){
    //     if(arr[i].x<midx-ans) break;
    //     midl=i;
    // }
    // for(int i=mid;i<=h;i++){
    //     if(arr[i].x>midx+ans) break;
    //     midh=i;
    // }
    // for(int i=midl;i<=midh;i++){
    //     for(int j=i+1;j<=midh;j++){
    //         ans=min(ans,d(arr[i],arr[j]));
    //     }
    // }
    int k=0;
    P arry[n];
    for(int i=l;i<=h;i++){
        if(arr[i].x>=arr[mid].x-ans&&arr[i].x<=arr[mid].x+ans){
            arry[k].x=arr[i].x;
            arry[k].y=arr[i].y; 
            k++;
        }  
    }
    sort(arry,arry+k,cmpy);
    for(int i=0;i<k;i++){
        for(int j=0;j<7;j++){
            if(i+j+1<k)ans=min(ans,d(arry[i],arry[i+j+1]));
        }
    }
    k=0;
    return(ans);
}
int main(){
    auto start = std::chrono::high_resolution_clock::now();

    P arr[10001];
    int N; bool con=true;
    while(cin>>N){
        if(N==0) break;
        else{
            for(int i=0;i<N;i++){
                cin>>arr[i].x>>arr[i].y;
                if(arr[i].x>40000||arr[i].y>40000||arr[i].x<0||arr[i].y<0)con =false;
            }
        }
        sort(arr,arr+N,cmpx);
        double d=closestpair(arr,N-1,0);
        if(d>10000||d==-1||con==false){
            cout<<"INFINITY "<<endl;
            con=true;
        }
        //else cout << setiosflags (ios::fixed) << setprecision (4)<< floor (d * 10000)/10000<< endl;
        else cout<<fixed<<setprecision(4)<<d<<endl;
    }
    auto end = std::chrono::high_resolution_clock::now();

    // 計算時間差
    std::chrono::duration<double> elapsed = end - start;

    // 顯示經過的時間（秒）
    std::cout << "Elapsed time: " << elapsed.count() << " seconds\n";
}
```
:::
p.s. 我在他的 main 頭尾加上了計時的程式碼 `auto start = ...`

---
理論上來說，方法 2 會比較好
但送到 APCScalss 後，反而 ==方法 1 的執行時間更短==，多次測試都得到一樣的結果
![image](https://hackmd.io/_uploads/BJUuSTD7ge.png)

這就是問題:
:::danger
為什麼時間複雜度低的好方法，反而需要更長的時間執行?
:::

---
### 時間複雜度的意義
$O(n^2)$ 是否永遠都比 $O(n \log n)$ 還慢？
1. $10\ n \log n > 0.1\ n^2,$ ` if n < 200`
   ![image](https://hackmd.io/_uploads/ryBTGyOmee.png =400x)
   演算法為了降低時間複雜度，通常會多做一些事情，減緩最耗時的步驟，
   當 n 不夠大時，這些耗時的部分就導致時間過久了
   :::info
   以這題來說，n=10000 已經夠大了，不是這個問題
   :::
2. 特例：==平均狀況== 與 ==最壞狀況==
   我們平常(或之前)討論的是 ==最壞狀況== 的時間複雜度，最久會需要多長的時間
   但這個最壞狀況不見得常常發生，比如:
   quicksort 的最壞狀況就是：每次選擇的切割點都是最大/最小值，導致算法會變成 $O(n^2)$
   
   ---
   來到這個最近點對問題，可以先思考第二種方法是想解決什麼問題。
   他想解決的問題是:
   > 第一種方法會導致 $O(n^2)$ 的搜尋次數 (搜尋範圍是 `[mid-d, mid+d]` 中的點。
   > 所以當範圍中的點 $n$ 很少，時間並不會太久；當範圍中的點 $n$ 很多(==最壞狀況==)，搜尋時間才會過久
   :::info
   那我就試著生成一個中間點很多的測資，然後分別用這兩種程式來跑就可以看出差異了 :+1:
   :::

### 生成特例測資
其實生測資的程式，我也是請 AI 生的 :+1:
:::spoiler gen.cpp
```cpp=

/*
幫我寫一個 CPP 程式，生成最近點對問題的特例測資
10000 個點
0 <= xy <= 40000

我需要的特例測資是，面對以下算法會過久的測資:

distance = min(left_min, right_min);
min_i <- min i that point[i].x >= point[mid].x - distance
max_i <- max i that ...
for(i = min_i; i<=max_i; i++) {
  for(j=i+1; j<=max_i; j++) {
    distance = min(distance(new_distance_from(point[i], point[j]));
  }
}
*/

#include <iostream>
#include <fstream>
#include <iomanip>
#include <cstdlib>

using namespace std;

int main() {
    ofstream fout("closest_pair_hardcase.txt");

    const int N = 10000;
    const int x_start = 20000;
    const int x_range = 8; // 所有 x 在 [20000, 20008] 之間
    const int y_step = 4;   // 保持距離不太近
    const int y_start = 0;

    fout << N << "\n";
    for (int i = 0; i < N; ++i) {
        int x = x_start + (rand() % x_range); // 產生接近的 x 值
        int y = y_start + i * y_step;         // 線性增加 y 值，保證距離夠大
        fout << x << " " << y << "\n";
    }

    fout.close();
    cout << "Test case generated: closest_pair_hardcase.txt" << endl;

    return 0;
}
```
:::
他做的事情就是，將點沿著 y 軸平分，所以大約間隔 4 個單位 $(40000 / 10000)$
> 以確保左右半邊找到的解 (`min_left`, `min_right`) 足夠大

接著將這些點的 x 座標均勻分布在 `[mid-4. mid+4]` 的範圍
> 以確保所有點都在中間線附近 (距離 4 以內)

### 測試兩份程式碼
1. 使用上述程式碼生成測資 `closest_pair_hardcase.txt`
2. 分別使用兩份 code 來執行，
   但為了避免貼上測資太慢導致的速度差異，會使用 ==輸出入轉向== 這個技術來輸入測資
   (細節在後)
3. $O(n^2)$ 的方法一，明顯跑比較久!
   ![image](https://hackmd.io/_uploads/BkjQrzuQxg.png)
   :::success
   結案 :+1:，特例的最壞狀況，確實會導致 `方法一` 速度太慢
   :::

#### 細節: ==輸出入轉向==
> 使用 `codeblocks` 或 `devc++` 等軟體，執行程式後都會生成一個 `.exe` 檔案，
> 這個檔案就是你寫的程式的 ==執行檔==，先假設為 `code.exe` 好了

> 你可以在 命令提示自元 (CMD) 找到這個 `code.exe` 並執行他，後續的流程就跟平常用 `codeblock` 跳出的黑框框一樣

> 但我們要做到的是輸入轉向，不是從鍵盤輸入，而是從 `.txt` 檔案輸入

> 方法是 `code.exe < closest_pair_hardcase.txt`
> 就能將文件的內容當作輸入，放到 code.exe 中


<div style="height: 500px;"></div>