專題·計算幾何入門【including 叉積,相交基本判斷,凸包,POJ P1113 Wall,旋轉卡殼,

初見安~最近疫情下不能開學,所以省選也延期了,就一直在測模擬賽,沒時間寫博客……【所以我滾回來補了。

一、叉積(叉乘)

在學過平面向量後我相信你們都會一個叫做點乘的東西。這裏就講一個叫做叉積的東西。

有向量\vec a =(x_1,y_1),\vec b=(x_2,y_2),則:\vec{a} \times \vec{b}=x_1y_2-x_2y_1

若叉積大於0,則\vec{a}\vec{b}的順時針方向,即\vec{a}逆時針旋轉可以得到\vec{b};反之亦然。等於0則兩向量共線(平行)。

叉積的數值還有一個含義就是以兩向量爲兩邊作的平行四邊形的面積。這個在旋轉卡殼的地方會用到。

二、相交的判定

1、直線與直線

直線是無限長的,所以看兩條直線的解析式(y=kx+b)就可以了。

2、線段與線段【不考慮兩線段重合的情況!】

線段之間我們要判斷是否互相跨立

兩條線段如果相交的話,可以發現一條線段的兩個端點一定是在另一條線段的兩邊

換一個角度,假設兩線段的兩端點分別是A_1,B_1,A_2,B_2,那麼\overrightarrow{A_1A_2} \times \overrightarrow{A_1B_2}\overrightarrow{B_1A_2} \times \overrightarrow{B_1B_2}一定異號。【因爲要在線段兩邊,所以如果一邊是順時針,那麼對於另一邊就一定是逆時針,叉積異號】這就是跨立

舉個例子:

對於第一個例子,如果以線段A_1B_1的兩個端點出發判斷,那麼不滿足上面所說的叉積異號,判定出不相交。但是以A_2B_2的兩個端點做判斷的話判定出來是相交的。所以判定的時候一定是互相跨立。

3、直線與線段

直線上取一點,跨立線段即可。【因爲有可能這一點剛好在線段上所以可以考慮取兩個點。

4、其他

上面三個是比較常用的。還有諸如求點/線段/折線/多邊形/圓 是否在一個 多邊形/圓內部之類的比較噁心的問題。比如求點是否在一個多邊形內部【在邊上or點上都判定爲在內部】,如果是凸多邊形的話可以往周圍作射線判定交點數量,凹多邊形的話……【我好像不會】。總之其他的各種變形是很多的,技巧是靈活的,遇到題再說吧。

 

三、凸包【二維】

1、定義

平面上有一些點,求一個周長最小的多邊形使所有的點都在多邊形內部,這個多邊形就是凸包

凸包一般是平面幾何內用到,更常用的是一個叫做凸殼的東西。但是原理是一樣的。

2、求凸包【只是想看方法的可以直接跳到法四】

法一O(n^3)【枚舉】兩點之間的每一條邊,如果使剩餘的所有點都在這條直線的一側,那麼這一定是凸包上的一條邊。
法二O(n^2)【分治】找到x軸上最大/最小的兩個點,這兩個點一定在凸包上;這兩點連線,找一個離這條線最遠的點,那麼這             個點一定也在凸包上。以此類推分治下去,找到凸包上的所有點。【這個複雜度不知道對不對……】
法三O(n^2)【步進】找到縱座標最低的一個點,向右作一條射線後逆時針旋轉,碰到的第一個點就是凸包上的下一個點;接下            來從第二個點做射線找第三個點,以此類推。複雜度似乎還是這麼多。
法四O(n)【Graham掃描法】以縱座標最低的一個點p_1建立座標系,將其他所有的點按夾角大小排序,即逆時針依次掃描每個            點p_2,p_3...p_n即連邊p_1-p_2,p_2-p_3,...p_{n-1}-p_n。若當前點p_k\overrightarrow{p_{k-1}p_k} \times \overrightarrow{p_kp_{k+1}}<0,即下一條邊需要順時針旋轉,              那麼點k一定不在凸包上了。我們可以設一個stack,依次把點放進去,遇到上述情況就把棧頂彈出去。

 

所以一般我們求凸包都是用的第四個方法O(n)掃描。

3、例題【板子

傳送門:POJ P1113 Wall

Sol:

這個題目沒有那麼裸,但是很容易看出來:其實求的就是給出的點的凸包周長加上一個單位圓的周長。因爲最後的圍牆就是凸包的沒條線段往外平移一個單位再拆一個圓用圓弧連接起來。所以求一個凸包,算一個周長就可以了。

看代碼——

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<queue>
#define maxn 1005
using namespace std;
typedef long long ll;
typedef double ld;
const ld eps = 1e-6, pai = acos(-1.0);

struct point {ld x, y;} p[maxn], s[maxn];
int n, tot = 0, D;

ld dis(point a, point b) {return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));}//返回兩點之間的距離
ld cross(ld x1, ld y1, ld x2, ld y2) {return x1 * y2 - x2 * y1;}//返回叉積
bool cmp(point a, point b) {//按夾角大小排序,eps是精度處理
	ld tmp = cross(a.x - p[1].x, a.y - p[1].y, b.x - p[1].x, b.y - p[1].y);
	if(tmp > eps) return true;//tmp是叉積。是以最低的那個點爲中點建立座標系
	if(tmp < -eps) return false;
	if(dis(p[1], a) < dis(p[1], b)) return true;
	return false;
}

signed main() {
	scanf("%d%d", &n, &D);
	for(int i = 1; i <= n; i++) {
		scanf("%lf%lf", &p[i].x, &p[i].y);
		if(i != 1 && (p[i].y < p[1].y || (p[i].y == p[1].y && p[i].x < p[1].x))) swap(p[1], p[i]);
	}//讓第一個點是最低點。
	
	s[++tot] = p[1]; sort(p + 2, p + 1 + n, cmp);//s是棧
	for(int i = 2; i <= n; i++) {//遍歷每一個點
		while(tot > 1 && cross(s[tot].x - s[tot - 1].x, s[tot].y - s[tot - 1].y, p[i].x - s[tot].x, p[i].y - s[tot].y) <= 0) tot--;//這裏就是前面說的淘汰點的情況。
		s[++tot] = p[i];
	}
	
	ld ans = 0.0; s[++tot] = p[1];//最後再把第一個點放進去方便計算
	for(int i = 1; i < tot; i++) ans += dis(s[i], s[i + 1]);
	printf("%.lf\n", ans + 2 * pai * D);
	return 0;
}

 

四、旋轉卡殼

【講真我至今都不知道這個詞到底是qiǎ qiào還是kǎ ké……就當他是qiǎ qiào吧】

看名字應該不清楚是什麼意思……但其實旋轉卡殼就是一個類似於思想的東西。

舉個例子:【傳送門:洛谷P1452】給你平面上一些點,求最遠兩點之間的距離。因爲這兩點一定在凸包上,所以也就是凸包的直徑。

我最開始的一個想法是:先找到離最底下的那個點最遠的點,然後O(n)兩個點一起轉,如果後面那個點走一步可以讓距離更長就走,這樣下來的所以距離中最長的就是凸包的直徑。但事實上並不是。因爲有下面這種情況:【我也忘了是不是這個點過不了emmm大致意思就是對於前一個點來說的最遠點的前一個點 其實才是對於後面那個點來說的最遠點。直接枚舉點會錯過。】

10
0 0
0 10000
100 1
199 2
100 9999 
199 9998
-900 100
-1799 200
-1799 9800
-900 9900

所以旋轉卡殼的正確方法是旋轉一個點和一條對邊,對邊的兩端點都有可能成爲直徑的另一端。
但是既然是枚舉一邊一點了,假設當前是邊,對面是點,如何確定點是否需要往前走一步?前面講到的叉積數值的面積含義這裏就用到了——假設當前邊是p_ip_{i+1},對面的點是p_j如果\overrightarrow{p_ip_{i+1}} \times \overrightarrow{p_ip_j} < \overrightarrow{p_ip_{i+1}} \times \overrightarrow{p_ip_{j+1}},那麼j點就可以往前走一步。正確性顯然【主要是我不會證QAQ】

上代碼康康吧。

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<queue>
#define maxn 50005
using namespace std;
typedef long long ll;
typedef double ld;
const ld eps = 1e-5;

int n, tot = 0;
struct point{ld x, y;}p[maxn], s[maxn];

//還是一樣的配方
ld cross(ld x1, ld y1, ld x2, ld y2) {return x1 * y2 - x2 * y1;}
ld dis(point a, point b) {return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));}
bool cmp(point a, point b) {
	ld tmp = cross(a.x - p[1].x, a.y - p[1].y, b.x - p[1].x, b.y - p[1].y);
	if(tmp > eps) return true;
	if(tmp < -eps) return false;
	if(dis(a, p[1]) < dis(b, p[1])) return true;
	return false;
}

signed main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) {
		scanf("%lf%lf", &p[i].x, &p[i].y);
		if(i != 1 && (p[i].y < p[1].y || (p[i].y == p[1].y && p[i].x < p[1].x))) swap(p[1], p[i]);
	}
	
	sort(p + 2, p + 1 + n, cmp); s[++tot] = p[1];
	for(int i = 2; i <= n; i++) {//求凸包
		while(tot > 1 && cross(s[tot].x - s[tot - 1].x, s[tot].y - s[tot - 1].y, p[i].x - s[tot].x, p[i].y - s[tot].y) <= 0) tot--;
		s[++tot] = p[i];
	}
	
	s[tot + 1] = p[1];
	ld ans = 0.0;
	for(int i = 1, j = 3; i <= tot; i++) {//現在開始旋轉卡殼
		point a = s[i], b = s[i + 1];//下面的while很長,建議看前文的向量叉積表示。
		while(cross(b.x - a.x, b.y - a.y, s[j].x - a.x, s[j].y - a.y) < cross(b.x - a.x, b.y - a.y, s[j + 1].x - a.x, s[j + 1].y - a.y)) j = j % tot + 1;
		ans = max(ans, max(dis(s[i + 1], s[j]), dis(s[i], s[j])));//取max
	}
	printf("%d\n", (int)(ans * ans));//該題目求的是向量直徑的平方。
	return 0;
}//39722

旋轉卡殼還有一個求最大三角形的題目,明天再補上傳送門……

迎評:)
——End——

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