专题·计算几何入门【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——

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