【筆記】二維凸包


Part -999 感謝列表

(排名不分先后)

Part 1 前言

首先說明一下,本人是剛學 \(\mathsf{OI}\) 的萌新,本學習筆記如有錯誤,並非有意,但仍然歡迎在討論去狂 \(\sf D\) 她。

關於圖片:本文所有圖片均為作者純手畫。

祝讀者有良好的閱讀體驗~

Part 2 何為計算幾何

學二維凸包,我們首先需要了解的就是計算幾何。

計算幾何,就是利用計算機建立數學模型解決幾何問題。

要用電腦解幾何題?數學好的同學們笑了。

我們並不是用計算機算數學卷子上的幾何題去了,而是解決一些更加復雜的幾何相關問題。

為了解決復雜且抽象的問題,我們一定要選擇合適的研究方法。對於計算機來說,給它看幾何圖形……

Part 3 二維凸包

Part 3.1 凸多邊形

凸多邊形是指所有內角大小都在 \([0, \pi]\) 范圍內的 簡單多邊形

Part 3.2 凸包

在平面上能包含所有給定點的最小凸多邊形叫做凸包。

其定義為:對於給定集合 \(X\) ,所有包含 \(X\) 的凸集的交集 \(S\) 被稱為 \(X\)凸包

\(\qquad\qquad\) —— OI-Wiki

其實我們可以把凸包看成一個拿橡皮筋圍成的一個圖形。

假設有一個布滿小凸起的板子:

3.1.2-1

我們要把這些凸起都圍起來,怎么圍呢?

顯然,最簡單方便的方法是這樣:

3.1.2-2

但是,我們知道,橡皮筋是有彈力的,所以橡皮筋會往里縮,直到這樣:

3.1.2-3

最外圈的凸起撐起了橡皮筋。

此時橡皮筋圍成的多邊形的頂點就是最外圈凸起所在的位置。

由此,我們就定義橡皮筋圍成的圖形為一個平面凸包。

那么,換一種定義,就為:

平面凸包是指覆蓋平面上 \(n\) 個點的最小的凸多邊形。

當然,我們發現在程序中卻無法模擬橡皮筋收縮的過程,於是有了下文的誕生。

Part 3.3 二維凸包的求法

在這里我們只講兩種主要的也是最常用的二維凸包的求法。

Part 3.3.1 Graham 算法

Graham 算法的本質:

Graham 掃描算法維護一個凸殼,通過不斷在凸殼中加入新的點和去除影響凸性的點,最后形成凸包。

凸殼:凸包的一部分。

此算法主要分為兩部分:

  • 排序
  • 掃描
Part 3.3.1.1 排序

我們先選擇一個 \(y\) 最小的點(如 \(y\) 相同選 \(x\) 最小),記為 \(p_1\)

剩下的點,按照極角的大小逆時針排序,記為 \(p_2,p_3,\dots, p_m\)

3.1.3.3.1-1

Part 3.3.1.2 掃描

(下列所說的左右等是指以上一條連線為鉛垂線,新的連線偏移的方向)

剛開始,我們的點集是這樣的:

3.1.3.3.1-2

\(p_1\) 為起始點。

隨后,\(p_2\) 准備入棧,由於棧元素很少,所以可以入棧。

3.1.3.3.1-3

再看 \(p_3\),因為 \(p_3\) 向左,符合凸包條件,入棧。

3.1.3.3.1-4

隨后 \(p_4\) 也一切正常,依然向左,入棧。

3.1.3.3.1-5

\(p_5\) 依然向左,入棧。

3.1.3.3.1-6

\(p_6\) 時,我們發現了點問題,就是不再是向左了,而是向右了,所以我們此時要將 \(p_5\) 出棧,\(p_6\) 入棧。

3.1.3.3.1-7

入棧后,我們發現,相對於 \(p_4\)\(p_6\) 依然是向右的,所以我們還要把 \(p_4\) 出棧,\(p_6\) 入棧。

8

接下來 \(p_7\) 沒有問題。

9

\(p_8\) 時,我們發現,也是向右的,所以將 \(p_7\) 出棧,\(p_8\) 入棧。

10

接下來 \(p_9\) 正常,入棧。

11

最后,我們再把最后一個點和第一個點連起來。

12

此時,我們的 Graham 算法的全過程就結束了。

時間復雜度為 \(O(n \log n)\)

Part 3.3.2 Andrew 算法

Graham 算法的一種進階。

假設我們有這些點:

1.PNG

首先把所有點以橫坐標為第一關鍵字,縱坐標為第二關鍵字排序。

2.PNG

相對於 Graham 算法來說,Andrew 算法排序更簡單,按 \(x, y\) 坐標排序,時間復雜度也更低(一般的坐標系中排序方法)。

首先將 \(p_1\) 入棧。

然后也將 \(p_2\) 入棧,\(p_2\) 可能在,也可能不在,等着之后判斷。

3.PNG

隨后,發現 \(p_3\) 偏右,所以我們將 \(p_2\) 出棧。

4.PNG

發現 \(p_4\) 依然偏右,\(p_3\) 出棧,\(p_4\) 入棧。

5.PNG

\(p_5\) 向右,\(p_4\) 出棧,\(p_5\) 入棧。

6.PNG

\(p_6\) 向左,入棧。

7.PNG

\(p_7\) 向右,\(p_6\) 出棧,\(p_7\) 入棧。

8.PNG

\(p_8\) 向右,\(p_7\) 出棧,繼續檢查發現相對於 \(p_5\) \(p_8\) 仍然向右,\(p_5\) 出棧,\(p_8\) 入棧。

9.PNG

此時,我們發現,凸包空了一半。

所以我們需要再從排序末尾的點(也就是 \(p_8\))出發,按照一模一樣的方式再算一遍就行了。

當然如果我們走過的點就不許要再走了(除了 \(p_1\)).

\(p_8\)\(p_7\),向左,\(p_7\) 入棧。

10.PNG

\(p_6\) 向右,\(p_7\) 出棧,\(p_6\) 入棧。

11.PNG

\(p_5\) 向左,入棧。

12.PNG

\(p_4\) 向左,入棧。

13.PNG

\(p_3\) 向右,\(p_4\) 出棧,對於 \(p_5\) \(p_3\) 依然向右,\(p_5\) 出棧,\(p_3\) 入棧。

14.PNG

\(p_2\) 向右,\(p_3\) 出棧,\(p_2\) 入棧。

15.PNG

最后將 \(p_2\)\(p_1\) 連起來。

16.PNG

至此,我們的 Andrew 算法就完成了!

時間復雜度:\(O(n \log n)\)

Part 3.4 實戰演練

Part 3.4.1 P2742 [USACO5.1]圈奶牛Fencing the Cows /【模板】二維凸包

先拿模板題練練手。

題目簡述:求一個二維凸包的周長。

拿 Graham 算法做即可。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<string>
#define line cout << endl
using namespace std;
const int NR = 1e5 + 5;
int n;
double ans;
struct point {
	double x, y;
};
point p[NR], ps[NR];
double dis (point pa, point pb) { //求兩點間距離
	return sqrt ((pb.x - pa.x) * (pb.x - pa.x) + (pb.y - pa.y) * (pb.y - pa.y));
}
double cp (point pa1, point pa2, point pb1, point pb2) { //求叉積
	return (pa2.x - pa1.x) * (pb2.y - pb1.y) - (pb2.x - pb1.x) * (pa2.y - pa1.y);
}
bool cmp (point px, point py) { //排序
	double num = cp (p[1], px, p[1], py);
	if (num > 0) return true;
	if (num == 0 && dis (p[0], px) < dis (p[0], py)) return true;
	return false;
}
int main () {
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> p[i].x >> p[i].y;
		if(i != 1 && p[i].y < p[1].y) { //去重
			swap (p[i].y, p[1].y);
			swap (p[i].x, p[1].x);
        }
	}
	sort (p + 2, p + n + 1, cmp);
	ps[1] = p[1]; //最低點是肯定在凸包里的
	int h = 1;
	for (int i = 2; i <= n; i++) {
		while (h > 1 && cp (ps[h - 1], ps[h], ps[h], p[i]) <= 0) { //判斷是向左還是向右,如果向右就出棧
			h--;
		}
		h++;
		ps[h] = p[i];
	}
	ps[h + 1] = p[1]; //最后一個點跟第一個點相連
	for (int i = 1; i <= h; i++) {
		ans += dis (ps[i], ps[i + 1]); //累加
	}
	printf ("%.2lf\n", ans);
	return 0;
}

Part 3.4.2 UVA11626 Convex Hull

這題好像拿 Graham 會 TLE?拿 Andrew罷,也是道模板題。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<string>
#define line cout << endl
using namespace std;
const int NR = 1e5 + 5;
const double eps = 1e-7;
int n;
struct point {
    double x, y;
    point () {}
    point (double a, double b) : x (a), y (b) {}
    bool operator < (const point &b) const {
        if (x < b.x) return 1;
        if (x > b.x) return 0;
        return y < b.y;
    }
    point operator - (const point &b) {
        return point (x - b.x, y - b.y);
    }
};
point p[NR], sp[NR];
int cmp (double x) {
    if (fabs (x) < eps) return 0;
    return x > 0 ? 1 : -1;
}
double dis (point a, point b) {
    return sqrt ((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
double cp (point a, point b) {
    return a.x * b.y - a.y * b.x;
}
int Andrew () {
    sort (p + 1, p + 1 + n);
    int len = 0;
    for (int i = 1; i <= n; i++) {
        while (len > 1 && cmp (cp (sp[len] - sp[len - 1], p[i] - sp[len - 1])) < 0) 
            len--;
        sp[++len] = p[i];
    }
    int k = len;
    for (int i = n - 1; i >= 1; i--) {
        while (len > k && cmp (cp (sp[len] - sp[len - 1], p[i] - sp[len - 1])) < 0)
            len--;
        sp[++len] = p[i];
    }
    return len;
}
int main () {
    int t;
    cin >> t;
    while (t--) {
        cin >> n;
        char c;
        for (int i = 1; i <= n; i++)
            cin >> p[i].x >> p[i].y >> c;
        int t = Andrew();
        cout << t - 1 << endl;
        for (int i = 1; i < t; i++) 
            printf ("%.0lf %.0lf\n", sp[i].x, sp[i].y);
    }
    return 0;
}

The End...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM