<style> .reveal .slides { text-align: left; font-size:28px; } </style> # Computational Geometry ## 計算幾何 Introduction to Competitive Programming 2025/05/29 ---- - 浮點數誤差 - What is Computational Geometry - 向量與內外積 - 面積 - 各種計幾問題 - 點與線段相交 - 點與線段距離 - 線段相交 - 點與多邊形 - 凸包 - 旋轉卡尺 - 最遠點對 - 最大三角形 - 極角排序 - 皮克定理 - 計算幾何與三分搜 - 最小包覆圓 --- ## 浮點數誤差 浮點數誤差在計算幾何中是非常頭痛的問題, 如果在比賽中沒有遇到,那是代表你有好的計算幾何技巧或者是非常幸運 而如果遇到了則代表你接下來到比賽結束或者 AC 之前,都會非常的痛苦煩躁。 ![image](https://hackmd.io/_uploads/ryerAag7A.png) ---- ## 浮點數誤差 EX1 ```cpp= double x = 0.1; double y = 0.2; double z = x+y; cout << fixed << setprecision(18) << z << '\n'; ``` 結果如下,很明顯看到會有誤差 ![image](https://hackmd.io/_uploads/SkLeA2Mflg.png =300x) ---- ## 浮點數誤差 EX2 你原本預計只需要跑十次,但其實他永遠不會停。 ```cpp! for (double i = 0.0; i != 1.0; i += 0.1) cerr << "runing....\n"; ``` ---- ## 解決辦法 小改一下浮點數的大小判斷方式 我們設定一個很小的值(eps),若是兩個數字的差小於eps,就當做他們相等。 ```cpp! for (double i = 0.0; abs(i - 1.0) > eps ; i += 0.1) cerr << "runing....\n"; ``` ---- ## eps如何決定 ![](https://i.imgur.com/gnfYSbi.png) eps通常根據題目的精度限制來決定,通常要設定的比題目要求的精度還要再精 ## 為什麼不設定超級小? 可能會導致TLE,或者剛剛那個判不了相等的問題發生 ---- 選擇適當的型態是最重要的 | 型態 | 精度 | | -------- | -------- | | float | $10^{-7}$ | | double | $10^{-16}$ | | long double | $10^{-19}$ | 通常題目要求的精度範圍為 $10^{-6}\sim 10^{-9}$ 一般來說都會用 double,如果題目時限允許或精度要求更大會使用 long double ---- ## 浮點數比較大小 ```cpp! int sgn(double x) { return (x > -eps) - (x < eps); } // retrun -1 -> 負數 // retrun 0 -> 0 // retrun 1 -> 正數 ``` 觀察一下可以發現這個函數會回傳 x 的正負關係 要比較兩個浮點數的時候只需要相減看正負就好了 ---- ## 避免浮點數 - 距離 由於浮點數誤差是個很麻煩的問題 因此在寫程式時,如果能避免使用浮點數則會盡量使用整數型態 像是比較平面上的距離的時候 距離公式 $\sqrt{x^2+y^2}$ 會開根號 但實際上只需求出 $x^2+y^2$ 即可判斷兩者距離大小 使用整數型態即可 ---- ## 避免浮點數 - 分數比較 給你兩個分數 $\frac{x_1}{y_1}$ 和 $\frac{x_2}{y_2}$ 問誰比較大 ? 我們也要注意避免使用除法除完再做比較。 可以發現只需要同乘 $y_1 \cdot y_2$ 就可以比較大小關係 判斷 $x_1\times y_2$ 和 $x_2\times y_1$ 的大小 ```cpp= if (x1 * y2 > x2 * y1) ``` ---- ## 避免浮點數 - 浮點數運算 給 n 個浮點數,且都是5位小數,求總和 ```cpp! 1.12345 2.23456 3.45678 ``` 像是這樣 如果題目為小數點固定後幾位,如固定小數點後一位 做法為全部數字成 100000 倍 ( 1.12345 -> 112345 ) 轉換成整數運算 ---- ## 避免浮點數 - 浮點數輸入 如果可以的話也盡量避免使用浮點數來輸入 浮點數cin很慢,而且浮點數的輸入會有誤差 就算輸入的數字是整數也不要使用浮點數來直接輸入,會TLE! ``` 4 1234 2345 3456 4567 ``` 請不要像下面這樣子! 你有極大的可能會TLE ```cpp! int n; cin >> n; for (i32 i = 0; i < n; i++) { double x; cin >> x; } ``` ---- ![image](https://hackmd.io/_uploads/BJYJx4WQ0.png) ![image](https://hackmd.io/_uploads/SyHglVbmA.png) ---- ## EX 輸入為 $n$ 固定小數後 2 位的小數,問總和? input ``` 4 122.31 87.96 2222.12 23.20 ``` 改成整數輸入,整數輸出 ```cpp= int n; cin >> n; int x, y; char ch; int sum = 0; for(int i = 0; i < n; i++){ cin >> x >> ch >> y; sum += x * 100 + y; } cout << sum / 100 << "." << setfill('0') << setw(2) << sum % 100 << '\n'; ``` ---- 講完以上非常重要的事情之後,可以來開始講計算幾何了 --- ## Computational Geometry ---- ## 向量 $\vec{v} = (a, b)$,代表往 $+x$ 軸走單位 $a$,往 $+y$ 軸走單位 $b$ 長度 $|\vec{v}|$ 為 $\sqrt{a^2+b^2}$ ### 向量四則運算 假設有兩向量 $v_{1} = (x_1, y_1), v_{2} = (x_2, y_2)$ - $v_1 + v_2 = (x_1 + x_2, y_1 + y_2)$ - $v_1 - v_2 = (x_1 - x_2, y_1 - y_2)$ - $v_1 × k = (x_1 × k, y_1 × k)$ (向量伸長成 $k$ 倍) - $v_1 ÷ k = (x_1 ÷ k, y_1 ÷ k)$ (向量縮短成 $k$ 倍) ---- ### 內積 $\vec{v_{1}} =(x_1, y_1), \vec{v_{2}}=(x_2, y_2)$ $\vec{v_{1}} \cdot \vec{v_{2}} = x_1x_2 + y_1y_2 = |\vec{v_1}||\vec{v_2}| cos \theta$ $(\theta$ 為兩向量夾角) 兩向量同向,則內積為正 (米色) 兩向量反向,則內積為負 (綠色) 兩向量垂直,則內積為 0 (分隔線上) ![image](https://hackmd.io/_uploads/SJ7uvbWQR.png =450x) ---- ### 外積 $\vec{v_{1}} =(x_1, y_1), \vec{v_{2}}=(x_2, y_2)$ $\vec{v_{1}} \times \vec{v_{2}} = x_1y_2 -x_2y_1=|\vec{v_1}||\vec{v_2}| sin \theta$ $(\theta$ 為兩向量夾角) $\vec{v_{1}} \times \vec{v_{2}} = - \vec{v_{2}} \times \vec{v_{1}}$ 具有負交換律 如果 $\vec{v_{1}}$ 到 $\vec{v_{2}}$ 為逆時鐘方向,則外積為正 如果 $\vec{v_{1}}$ 到 $\vec{v_{2}}$ 為順時鐘方向,則外積為負 兩向量平行則外積為 0 ![cross](https://hackmd.io/_uploads/H1heBb-QR.png =500x) ---- 同時外積取絕對值(| $\vec{v_{1}} \times \vec{v_{2}}$ |)等同於兩向量夾起來的平行四邊形面積 ![](https://i.imgur.com/VCgZcgY.png =300x) 所以兩向量外積 / 2 等於求夾起來的三角形面積 ---- 點(向量)的儲存方式 利用重載運算子來實現剛剛所說的計算 利用template,因為有時候計算只需要整數,有時候需要浮點數 ```cpp! using ld = long double; template<class T> struct pt{ T x,y; pt(T _x,T _y):x(_x),y(_y){} pt():x(0),y(0){} pt operator * (T c){ return pt(x * c, y * c); } pt operator / (T c){ return pt(x / c, y / c); } pt operator + (pt a){ return pt(x + a.x, y + a.y); } pt operator - (pt a){ return pt(x - a.x, y - a.y); } T operator * (pt a){ return x * a.x + y * a.y; } // 內積 T operator ^ (pt a){ return x * a.y - y * a.x; } // 外積 bool operator < (pt a) const { return x < a.x || (x == a.x && y < a.y); } }; // using Pt = pt<long long>; // using Pt = pt<long double>; ``` ---- ### 多邊形面積 (鞋帶公式) 一個 $n$ 個點的多邊形($P_1, P_2,... P_n$) 可以拆成很多三角形,再分別套用外積公式 ($x_1y_2 -x_2y_1$) $\frac{1}{2}|\sum\limits_{i=1}^{n} \overrightarrow {OP_{i}} \times \overrightarrow {OP_{i+1}}|$ 複雜度 $O(N)$ ---- ## 小技巧 如果你發現你最後需要 / 2 ,像是鞋帶公式(整數點的情況)。 你其實可以完全避免掉浮點數 be like: ```cpp! int area; if(area & 1) cout << area / 2 << ".5" << '\n'; else cout << area / 2 << '\n'; ``` ---- ## 三點共線 如果 3 個點連成一點線,則圍起來的三角形面積為 0 則直接判斷外積是否為 0 即可 ```cpp= bool collinearity(const Pt& a, const Pt& b, const Pt& c){ return sgn((b - a) ^ (c - a)) == 0; } ``` ---- ## 判斷點是否在線段上 必要條件為點與線段共線 (外積為 0) 且 兩端點往點的方向會相反 (內積為負) ![](https://hackmd.io/_uploads/BybgZJNrh.png) ```cpp! auto ori(Pt a, Pt b, Pt c) { return (b - a) ^ (c - a); } struct Line { Pt a, b; Pt dir() const { return b - a; } }; bool PtOnSeg(Pt p, Line L) { return sgn(ori(L.a, L.b, p)) == 0 and sgn((p - L.a) * (p - L.b)) <= 0; } ``` ---- ## 點與線段最近距離 兩種 case 1. 點 $C$ 到線段 $\overline{AB}$ 的其中一個端點最近 2. 點 $C$ 到線段 $\overline{AB}$ 的垂直距離 ![](https://hackmd.io/_uploads/SJS0pRmH3.png =x300) ![](https://hackmd.io/_uploads/BJPhnC7B2.png =x300) ---- case 1. 點 $C$ 到線段 $\overline{AB}$ 的其中一個端點最近 ![](https://hackmd.io/_uploads/SJS0pRmH3.png =x300) 可以分別判斷 - $\overrightarrow{AB}$ 與 $\overrightarrow{AC}$ - $\overrightarrow{BA}$ 與 $\overrightarrow{BC}$ 判斷是否為反向(內積 < 0),若反向代表是case 1 ---- case 2. 點 $C$ 到線段 $\overline{AB}$ 的垂直距離 ![](https://hackmd.io/_uploads/BJPhnC7B2.png =x300) 垂直距離等價於 $\triangle ABC$ 以 $\overline{AB}$ 為底時的高 $\triangle ABC$ 面積 * 2 除以 $\overline{AB}$ 的長度即可 ---- code ```cpp! ld PtSegDist(Pt p, Line l) { ld ans = min(abs(p - l.a), abs(p - l.b)); if (sgn(abs(l.a - l.b)) == 0) return ans; if (sgn((l.a - l.b) * (p - l.b)) < 0) return ans; // case1 if (sgn((l.b - l.a) * (p - l.a)) < 0) return ans; // case1 return min(ans, abs(ori(p, l.a, l.b)) / abs(l.a - l.b)); // case2 } ``` ---- ## 兩線段相交與否 如果相交則分成兩種 case 1. 交點在其中一個端點上 2. 交點不在端點上 ![](https://hackmd.io/_uploads/ryV8Zk4S3.png =x200) ![](https://hackmd.io/_uploads/H1iHWJVrh.png =x200) ---- ## case1 交點在其中一個端點上 ![](https://hackmd.io/_uploads/ryV8Zk4S3.png =x200) 判斷這四種情況 - $\texttt{PtOnSeg(m.a, l)}$ - $\texttt{PtOnSeg(m.b, l)}$ - $\texttt{PtOnSeg(l.a, m)}$ - $\texttt{PtOnSeg(l.b, m)}$ 只要其中一個符合就代表有相交 ---- ## case2 交點不在端點上 ![](https://hackmd.io/_uploads/H1iHWJVrh.png =x200) 呈現交叉情況 則點 A、B 會在線段 $\overline{CD}$相異側 則點 C、D 會在線段 $\overline{AB}$相異側 如果在相異側則外積相乘為負 ---- code ```cpp! auto ori(Pt a, Pt b, Pt c) { return (b - a) ^ (c - a); } bool PtOnSeg(Pt p, Line L) { return sgn(ori(L.a, L.b, p)) == 0 and sgn((p - L.a) * (p - L.b)) <= 0; } int PtSide(Pt p, Line L) { return sgn(ori(L.a, L.b, p)); } bool isInter(Line l, Line m) { if (PtOnSeg(m.a, l) or PtOnSeg(m.b, l) or PtOnSeg(l.a, m) or PtOnSeg(l.b, m)) return true; // case1 return PtSide(m.a, l) * PtSide(m.b, l) < 0 and PtSide(l.a, m) * PtSide(l.b, m) < 0; // case2 } ``` ---- ## 點在多邊形內部 逆時針枚舉多邊形的邊,判斷點有沒有在邊上 同時從點沿著 水平方向 向左向右發射一條射線,如果有一個 向量$\vec{AB}$(逆時針的邊向量) 和他有接觸,那就要判斷這個點在向量$\vec{AB}$的哪一側,左側的話就要將計數+1,右側的話就要將計數-1, ---- 我們觀察可以發現,不管是凹多邊形還是凸多邊形,計數為 0 一定在外部,否則在內部。 ![image](https://hackmd.io/_uploads/ryLctuSGxe.png =450x) ![image](https://hackmd.io/_uploads/BJccF_SMel.png =450x) ---- 詳細看代碼 ```cpp! int inPoly(Pt p, const vector<Pt> &P) { const int n = P.size(); int cnt = 0; for (int i = 0; i < n; i++) { Pt a = P[i], b = P[(i + 1) % n]; if (PtOnSeg(p, {a, b})) return 1; // on edge if ((sgn(a.y - p.y) == 1) ^ (sgn(b.y - p.y) == 1)) cnt += sgn(ori(a, b, p)); } return cnt == 0 ? 0 : 2; // out, in } ``` --- ## 凸包 你有 $n$ 個點,然後你要找出一個凸多邊形可以圍住這 $n$ 個點且面積最小, 這個凸多邊形稱為凸包 你也可以想成凸包是用一條橡皮筋把牆壁上的圖釘圈起來的範圍 ![](https://i.imgur.com/kJJ2Pjj.png) ![](https://i.imgur.com/CAJDtk7.png =600x) ---- ## Andrew's Monotone Chain (我們常用的做法) 作法為分別求出上下凸包,求完之後再和再一起, 上半凸包+下半凸包=完整的凸包 ![](https://i.imgur.com/q6IDkLt.png) ---- ## 過程 首先sort(先按照x,再按照y) 可以發現 __最小的點__ 和 __最大的點__ 都一定在凸包上面 觀察到凸包上如果逆時針走,每次都只會是左轉的,如果有右轉就代表他不在凸包上面。 我們先升序找下凸包,再降序找上凸包 ---- 我們用一個單調棧來維護。 一旦發現即將進棧的點(P)和棧頂的兩個點(S₁、S₂,其中 S₁ 為top)行進方向向右旋轉,即外積小於 0,那S₁就不會在凸包上面 $\overrightarrow{S_2S_1}\times\overrightarrow{S_2P}<0$ 看圖很明顯可以發現,若是有p這樣的點,S₁不可能會在下凸包上面 所以S₁會被出棧,反復操作到沒有外積小於0的情況發生,再把 點(P) 進棧,這樣可以保持整個棧的單調性(都只會左轉) ![image](https://hackmd.io/_uploads/SklliOSMex.png =300x) ---- ## 模版 ```cpp! vector<Pt> Hull(vector<Pt> P) { sort(all(P)); P.erase(unique(all(P)), P.end()); P.insert(P.end(), P.rbegin() + 1, P.rend()); vector<Pt> stk; for (auto p : P) { auto it = stk.rbegin(); while (stk.rend() - it >= 2 and \ ori(*next(it), *it, p) <= 0 and \ (*next(it) < *it) == (*it < p)) { it++; } stk.resize(stk.rend() - it); stk.push_back(p); } stk.pop_back(); return stk; } ``` --- ## 旋轉卡尺 ---- ## 最遠點對 在平面上有 $N$ 個點,找出相距最遠的兩個點。 - $N\leq10^5$ 可以發現最遠點對會發生在凸包上面。 ---- 可以發現對於一條邊來說,其他的點和邊的距離具有凸性。 ![image](https://hackmd.io/_uploads/H1XrnGNMxx.png =600x) 因為具有凸性,當我們逆時針枚舉每一條邊的時候,對應這條邊的最遠點也只會逆時針移動。 最遠點$P$ 離這條邊越遠,那這個點 $P$ 距離這條邊的兩個端點也會越遠。 ---- 利用雙指針的技巧,可以實現 $O(NlogN + N)$ 找到最遠點對。 - 凸包 $O(NlogN)$ - 旋轉卡尺 $O(N)$ ```cpp! auto abs2(Pt a) { return a * a; } auto RotatingCalipers(const vector<Pt> &hull) { // 最遠點對 回傳距離平方 int n = hull.size(); auto ret = abs2(hull[0]); ret = 0; if (hull.size() <= 2) return abs2(hull[0] - hull[1]); for (int i = 0, j = 2; i < n; i++) { Pt a = hull[i], b = hull[(i + 1) % n]; while(ori(hull[j], a, b) < (ori(hull[(j + 1) % n], a, b))) j = (j + 1) % n; chmax(ret, abs2(a - hull[j])); chmax(ret, abs2(b - hull[j])); } return ret; } ``` ---- ## 極角排序 給定 $N$ 個點,沿著原點 順/逆時針排序。 ![](https://hackmd.io/_uploads/HkAmw1VSh.png) ---- ## 做法1 --- 照角度排序 可以使用內建函式 $atan2(y, x)$ 會回傳向量的弧度 弧度 $\times \frac{180}{π} =$ 角度 ```cpp= bool cmp(const Pt& lhs, const Pt rhs){ return atan2(lhs.y, lhs.x) < atan2(rhs.y, rhs.x); } sort(P.begin(), P.end(), cmp); ``` $atan2(y, x)$ 的回傳值 界在 $(-\pi, \pi]$ 排序完的順序看圖 ![image](https://hackmd.io/_uploads/HJI0hdrGgg.png =300x) 但這個函數其實有點慢。 ---- ## 更快的做法 --- 外積判斷 用 X軸 把點分成上下兩部分, 如果兩個點屬於同一個部分,並且 $\vec{a} \times \vec{b} > 0$ 也就是 $\vec{a}$ 到 $\vec{b}$ 是逆時針,就代表 $arg(a) < arg(b)$ 細節請看code ```cpp! ld arg(Pt x) { return atan2(x.y, x.x); } bool argcmp(Pt a, Pt b) { // arg(a) < arg(b) int f = (Pt{a.y, -a.x} > Pt{} ? 1 : -1) * (a != Pt{}); int g = (Pt{b.y, -b.x} > Pt{} ? 1 : -1) * (b != Pt{}); return f == g ? (a ^ b) > 0 : f < g; } ``` ---- ## 經典例題 給平面上 $N$ 個點,每個點有點權 $w_i$ 選其中兩個點連成直線把平面切成兩半,選哪兩個點切能讓兩邊的點權差最少? - $N\le 2000$ - $|x_i|, |y_i|\le 10^9$ - $w_i\le 10^9$ - 保證不會有三點共線 ---- 很明顯我們可以找一個點當做原點極角排序一下 然後就可以照著排序好的順序枚舉另一個點和原點連成直線,轉一圈找到最小點權差 這樣是$O(NlogN + N)$ 但是每一個點都要做一次 $\rightarrow$ $O(N^2logN + N^2)$ ---- ![](https://hackmd.io/_uploads/HkpTMoVS2.png =300x) ![](https://hackmd.io/_uploads/SyFYXj4Bn.png =300x) 旋轉的時候每個節點最多只會變成另一半邊一次而已 旋轉的時候可以維護每個節點在哪一半 ---- ## 實作技巧 把其中一半的點旋轉$180^\circ$,並且打好標記,你會比較容易實作 ![](https://hackmd.io/_uploads/S1fSHjVS3.png =x350) $\to$ ![](https://hackmd.io/_uploads/SkNr8s4Sh.png =x350) --- ## 皮克定理 (Pick Theorem) ### **$A = i + b/2 − 1$** - $A$: 多邊形的面積 - $i$: 多邊形內部的格子點 - $b$: 多邊形邊上的格子點 --- ## 三分搜 找連續凸函數的極值 ---- ## 複習 如果要找函數的最小值 ```cpp! using ld = long double; ld lo = 0, hi = mx; for (i32 t = 0; t < 200; t++) { ld ml = lo + (hi - lo) / 3.0; ld mr = hi - (hi - lo) / 3.0; if (f(ml) < f(mr)) hi = mr; else lo = ml; } ``` ![image](https://hackmd.io/_uploads/BJ39Mw4zle.png =600x) ---- ## [傳送帶](https://www.luogu.com.cn/problem/P2571) 在一個 2 維平面上有兩條傳送帶分別為線段 $\overline{AB}$ 和線段 $\overline{CD}$。 如果在$\overline{AB}$ 上的移動速度為 $P$,在 $\overline{CD}$ 上的移動速度為 $Q$,在平面上的移動速度 $R$。 現在想從 $A$ 點走到 $D$ 點,最少需要走多久? - 第一行包含 4 個整數,分別表示點 A 和點 B 的座標: - $A_x, A_y, B_x, B_y$ - 第二行包含 4 個整數,分別表示點 C 和點 D 的座標: - $C_x, C_y, D_x, D_y$ - 第三行包含 3 個整數: - $P, Q, R$ ---- 其實就是要決定在$\overline{AB}$ 和 $\overline{CD}$ 上走多久 ![image](https://hackmd.io/_uploads/BJPr8m-70.png =600x) ---- $x$ 是在 $\overline{AB}$ 上走的時間 $y$ 是在 $\overline{CD}$ 上走的時間 $f_1(x)$ 計算過了$x$單位時間他在$\overline{AB}$上的哪裡 $f_2(y)$ 計算過了$y$單位時間他在$\overline{CD}$上的哪裡 可以列出式子 $f(x, y) = \frac{\mathrm{dis}(f_1(x), f_2(y))}{R} + x + y$ 對這個式子做三分搜套三分搜 ---- 細節看code ```cpp! i64 p, q, r; Line AB, CD; i64 x1, y1, x2, y2; xin >> x1 >> y1 >> x2 >> y2; AB.a = Pt(x1, y1); AB.b = Pt(x2, y2); xin >> x1 >> y1 >> x2 >> y2; CD.b = Pt(x1, y1); CD.a = Pt(x2, y2); xin >> p >> q >> r; auto fff = [&](ld x, ld y) -> ld { Pt abpt = AB.a + unit(AB.dir()) * (p * x); Pt cdpt = CD.a + unit(CD.dir()) * (q * y); return (abs(abpt - cdpt) / (ld)r) + x + y; }; ld mxx = abs(AB.dir()) / (ld)p; ld mxy = abs(CD.dir()) / (ld)q; auto besty = [&](ld x) -> ld { ld lo = 0, hi = mxy; for (i32 ii = 0; ii < 200; ii++) { ld ml = lo + (hi - lo) / 3.0; ld mr = hi - (hi - lo) / 3.0; if (fff(x, ml) < fff(x, mr)) hi = mr; else lo = ml; } return fff(x, lo); }; ld lo = 0, hi = mxx; for (i32 ii = 0; ii < 200; ii++) { ld ml = lo + (hi - lo) / 3.0; ld mr = hi - (hi - lo) / 3.0; if (besty(ml) < besty(mr)) hi = mr; else lo = ml; } cout << besty(lo) << '\n'; ``` ---- ## 最小圓覆蓋 平面上給 n 個點,求出半徑最小的圓要包住所有的點。 求出圓心位置與與最小半徑 - $1\le n\le 10^5$ - $1\le |x|, |y|\le 10^9$ <img src="https://hackmd.io/_uploads/Skf2QDEzel.png" width="450" style="background:white; padding:4px;"> ---- ## 三分套三分 我們把最小圓覆蓋問題轉成這個式子。 如果能找到這個式子的最小值,那就找到了這個圓。 $$ f(x,y) \;=\; \max\limits_{1 \le i \le n} \sqrt{(x - x_i)^2 + (y - y_i)^2} $$ 並且這個函數是一個單峰函數 ---- 我們一樣用上一題的方式三分套三分 ---- 細節看code ```cpp! i32 n; cin >> n; vector<Pt> vec; for (i32 i = 0; i < n; i++) { double x, y; cin >> x >> y; vec.emplace_back(x, y); } auto f = [&](ld x, ld y) -> ld { ld ret = 0; for (i32 i = 0; i < n; i++) chmax(ret, abs(Pt(x, y) - vec[i])); return ret; }; ld cirx, ciry; auto besty = [&](ld x) -> ld { ld lo = -1e5, hi = 1e5; while ( sgn(lo - hi) < 0 ) { ld ml = lo + (hi - lo) / 3.0; ld mr = hi - (hi - lo) / 3.0; if (f(x, ml) < f(x, mr)) hi = mr; else lo = ml; } ciry = lo; return f(x, lo); }; ld lo = -1e5, hi = 1e5; while ( sgn(lo - hi) < 0 ) { ld ml = lo + (hi - lo) / 3.0; ld mr = hi - (hi - lo) / 3.0; if (besty(ml) < besty(mr)) hi = mr; else lo = ml; } cirx = lo; cout << besty(cirx) << '\n'; cout << cirx << ' ' << ciry << '\n'; ``` 但是我們很快發現這個方法不太好 因為精度很低,又有一點慢 ---- 底下這題精度要求 $1e-9$,很明顯三分搜精度不夠 ![image](https://hackmd.io/_uploads/ByA5FOVzee.png =450x) 所以我們講另一個辦法 ---- ## $Welzl's 算法$ ---- ## 原理 - 如果第 $i + 1$ 個點不在前 $i$ 個點的最小包覆圓上,那第 $i + 1$ 個點一定在新的圓上。 - 三個點可以確定一個圓 ---- ## 算法步驟 我們從所有未處理的點裡面隨機挑一個點加入答案集合中,然後維護這個答案集合,然後反復操作直到所有點都加進答案集合。 1. 如果我們挑的點在目前的最小包覆圓裡面,那很明顯可以直接加入,不需要再維護這個集合。 2. 如果我們挑的點不在目前的最小包覆圓裡面,那我們就需要找到新的最小包覆圓。 ---- ## 怎麼找到新的最小包覆圓? 我們可以知道新加入的那個點一定在新的最小包覆圓的上面 那這樣我們可以枚舉答案集合裡面所有其他的點,找到另外兩個點可以構造出一個圓包住所有答案集合內的點。(三點定圓) (兩層迴圈枚舉另外兩個點) ---- 詳細看code ```cpp! Pt Center(Pt a, Pt b, Pt c) { Pt x = (a + b) / 2; Pt y = (b + c) / 2; return LineInter({x, x + rotate(b - a)}, {y, y + rotate(c - b)}); } Cir MEC(vector<Pt> P) { mt19937 rng(time(0)); shuffle(all(P), rng); Cir C = {P[0], 0.0}; for (int i = 0; i < P.size(); i++) { if (C.inside(P[i])) continue; C = {P[i], 0}; for (int j = 0; j < i; j++) { if (C.inside(P[j])) continue; C = {(P[i] + P[j]) / 2, abs(P[i] - P[j]) / 2}; for (int k = 0; k < j; k++) { if (C.inside(P[k])) continue; C.o = Center(P[i], P[j], P[k]); C.r = abs(C.o - P[i]); } } } return C; } ``` 有三圈 for 迴圈 $\cancel{O(N ^ 3)} \rightarrow {O(N)}$ 因為隨機挑選的話其實只會有$\frac 3 n$的幾率會挑中圓的支撐點(三點定圓) $O(N)\times\frac{3}{N}\times O(N)\times\frac{3}{N}\times O(N)\rightarrow O(N)$
{"title":"Computational Geometry","description":"Introduction to Competitive Programming","contributors":"[{\"id\":\"3a4ab387-f23d-466f-a3db-98c85d8d5efb\",\"add\":19613,\"del\":3076},{\"id\":\"3d52dec2-fae6-4cd0-b9d8-07145517e0ba\",\"add\":3,\"del\":0},{\"id\":\"ee332997-4bcb-4d2e-9fa3-4d7d0277a651\",\"add\":1,\"del\":0,\"latestUpdatedAt\":1755751544223}]"}
    332 views
   Owned this note