二维凸包学习笔记
初识二维凸包
- 凸包是计算机图形学中的一个概念。
- 但是讲大白话的话就是:
- 给你一堆点,然后用一个多边形把这些点圈起来,这个多边形就是凸包。(当然多边形周长是最小的。)
- 求凸包有好多种方法,(Graham)法较为常用。
- (Graham)葛立恒,曾经是美国数学学会((AMS))主席。
前置知识
- 简单平面几何知识
- 叉积的概念或基础的线性代数中的行列式知识
Graham算法
本质
- (Graham)扫描算法维护一个凸壳,通过不断地在凸壳中加入新的点和取出影响凸性的点最后形成凸包。
- 凸壳:凸包的一部分。
算法流程
(1:)排序
- 选择一个(y)值最小的点(如果有相同的(y),则选择(x)最小),记为(P_1)。
- 那其实就相当于选了一个最下边的点,而且这个点一定在凸包的边上。
- 剩下的点按照极角的大小逆时针排序,编号为(P_2)~(P_n)
(2:)扫描
- 逆时针顺序扫描(P_1)$P_2$,$P_2$(P_3,...,P_n)~(P_1),如果扫描的那条线是逆时针,则确定他是凸包的点,否则不是。
- 这么说比较抽象,直接来看过程图示模拟。
过程模拟
-
首先我随机设置了一些点,如图所示。
-
之后我先取(y)值最小的点,很显然是最下方的那个点,将他设置为(P_1)。
-
之后按照极角排序
-
这时候我们就给点编好号了
-
我们先扫描(P_1)到(P_2)。
-
然后扫描(P_2)~(P_3)
-
我们发现新的这一条先也是逆时针的,所以我们将(P_3)候选。
-
当然对于整幅图,(P_3)不在凸包上,但是对于(P_1,P_2,P_3)来讲,(P_3)在凸包上。
-
接下来扫描(P_3)~(P_4)
-
我们发现新的扫描的(P_3)$P_4$顺时针了,说明$P_3$不应该候选,$P_4$应该候选,所以将$P_2$(P_3)删除后加入(P_2)~(P_4)。
-
之后我们看(P_4)到(P_5)
-
这时候是逆时针,将(P_5)候选。
-
接下来扫描(P_6)
-
这时候顺时针了,所以(P_5)不应该候选,删除(P_4)$P_5$后加入$P_4$(P_6)。
-
然后看(P_7)
-
逆时针,(P_7)候选
-
最后连接(P_1)回来即可
代码实现
-
我们发现每次扫描一个出点的时候都是先让他可能候选,当他下一条出的边依然是逆时针就将他确定候选,所以这时候用一个栈来保存点就可以了。
-
当他可能候选的时候入栈,如果不是的话(pop)栈即可。
-
对于顺时针还是逆时针,我们运用叉积来判断。(如果这里学过线性代数应该就能很快理解代码里的VP函数)
-
模板题:洛谷2742二维凸包
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n;
struct Node{
double x, y;
}p[maxn], s[maxn];
//p是点 s数组模拟栈
//二维向量叉积公式a(x1, y1), b(x2, y2)
//a × b = (x1y2 - x2y1)
double vp(Node a1, Node a2, Node b1, Node b2){
//VectorProduct 向量叉积
return (a2.x-a1.x)*(b2.y-b1.y) - (b2.x-b1.x)*(a2.y-a1.y);
}
double dis(Node a, Node b){
//返回两点距离
return sqrt((b.y-a.y)*(b.y-a.y)+(b.x-a.x)*(b.x-a.x));
}
bool cmp(Node a, Node b)
{
double tmp = vp(p[1], a, p[1], b);
if(tmp > 0) return 1;
//叉积为0 说明两点共线
//让离远点远的点排在靠后的地方
if(tmp == 0 && dis(p[0], a) < dis(p[0], b)) return 1;
return 0;
}//极角排序
int 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))) //让p1保存最靠下的点
{
double tmp;
tmp = p[1].y; p[1].y = p[i].y; p[i].y = tmp;
tmp = p[1].x; p[1].x = p[i].x; p[i].x = tmp;
}
}
sort(p+2, p+1+n, cmp);
int cnt = 1; //stack.size()
s[1] = p[1];
for(int i = 2; i <= n; i++)
{
//判断上一个点会不会不合法
while(cnt > 1 && vp(s[cnt-1], s[cnt], s[cnt], p[i]) <= 0)
cnt--;
s[++cnt] = p[i];
}
s[cnt+1] = p[1]; //最后回到起点
//此时栈中存的是所有凸包上的点
double ans = 0;
for(int i = 1; i <= cnt; i++)
ans += dis(s[i], s[i+1]);
printf("%.2f
", ans);
return 0;
}