抽象解释:在一个实数向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包。X的凸包可以用X内所有点(X1,…Xn)的凸组合来构造(来自百度百科);
简单理解:就是一个点集中最外围的部分点,这些最外围的点的集合,就是包围原点集的最小凸多边形的顶点组成的集合,称为原点集的凸包。下图蓝线相连的5个点即为凸包
Graham算法
寻找凸包的算法有很多种,Graham Scan 算法是一种十分简单高效的二维凸包算法,能够在 O(nlogn) 的时间内找到凸包
1.将给定的集合的点按X坐标升序排序,X相同则按Y升序排序;
2.创建凸包上部:
将排序后的点按从小到大顺序加入凸包A,若新加入的点使凸包A不再是凸多边形,则逆序删除之前加入的点,直到重新变成凸多边形;
举例说明:首先加入两个点A,B
然后插入第三个点 C,并计算向量 AB × BC的向量积,却发现向量积系数小于(等于)0,也就是说向量 BC 在 AB 的顺时针方向上,(对叉乘而言,逆时针方向为正),显然当BC在AB的顺时针方向时,B的存在使得下边界不凸,所以去掉B。
按照这样的方法依次扫描,找出下边界,再举一个例子
3.创建凸包下部:
将排序后的点按从大到小顺序加入凸包B,若新加入的点使凸包B不再是凸多边形,则逆序删除之前加入的点,直到重新变成凸多边形;判断方法与上壳一致
4.合并上下双壳,得到最终结果。
模板
struct point {
double x, y;
point(ll a = 0, ll b = 0) { x = a, y = b; }
point operator +(const point p) { return point(x + p.x, y + p.y); }
point operator -(const point p) { return point(x - p.x, y - p.y); }
double operator*(const point p) { return x * p.x + y * p.y; }//内积
double mul(const point p) { return x * p.y - p.x*y; }//叉乘
};
point ps[Max];
bool cmp(point p, point q) {
if (p.x == q.x) return p.y < q.y;
else return p.x < q.x;
}
//求凸包
vector<point> convexHull(int n) {
sort(ps, ps + n, cmp);
int k = 0; //凸包的顶点数目
vector<point> qs(n * 2);//构造中的凸包
//构造中的凸包下侧
for (int i = 0; i < n; i++) {
//(qs[k - 1] - qs[k - 2]):点k-2到k-1的向量
//(ps[i] - qs[k - 1]):当前判断的点i到k-1的向量
//mul:叉乘,结果为负说明k-2,k-1,i不能构成一个凸集
while (k > 1 && (qs[k - 1] - qs[k - 2]).mul(ps[i] - qs[k - 1]) <= 0)k--;
qs[k++] = ps[i];
}
for (int i = n - 2, t = k; i >= 0; i--) {
//n-2:最右侧的点肯定在凸集内部
while (k > t && (qs[k - 1] - qs[k - 2]).mul(ps[i] - qs[k - 1]) <= 0)k--;
qs[k++] = ps[i];
}
qs.resize(k - 1);
return qs;
}
旋转卡壳Rotating calipers
所谓的旋转卡壳法,就是在凸包上旋转扫描的方法,一般旋转卡壳是用来求取凸集的直径。而凸包的直径,往往可以用来求平面上的最远点对之类的问题。
适用场景
(1)计算距离
- 凸多边形直径
- 凸多边形宽
- 凸多边形间最大距离
- 凸多边形间最小距离
(2)外接矩形
- 最小面积外接矩形
- 最小周长外接矩形
(3)三角剖分
- 洋葱三角剖分
- 螺旋三角剖分
- 四边形剖分
(4)凸多边形属性
- 合并凸包
- 找共切线
- 凸多边形交
- 临界切线
- 凸多边形矢量和
定义
对踵点对: 如果两个点 p 和 q 是凸多边形边上的点。而且他们在两条平行切线上, 那么他们就形成了一个对踵点对。 以下是三种不同的对踵点对,注意一对不一定只有两个
先写一个计算距离的,其他的旋转思路大致相同。假设最远点对是
p
p
p和
q
q
q,那么
p
p
p就是点集中
(
p
−
q
)
(p-q)
(p−q)方向最远的点,而
q
q
q是点集
(
q
−
p
)
(q-p)
(q−p)方向最远的点。因此,可以按照逆时针逐渐改变方向,同时枚举出所有对于某个方向上最远的点对。那么最远点对一定也包含于其中。在逐渐改变方向的过程中,对踵点对只有在方向等于凸包某条边的法线方向时发生变化,此时点将向凸包上对应的相邻点移动。令方向逆时针旋转一周,那么对踵点对也在凸包上转了一周,这样就可以在凸包顶点数的线性时间内求得最远点对。
void Rotation() {
vector<point> qs = convexHull(N);
int n = qs.size();
if (n == 2) { cout << int(dist(qs[0], qs[1])) << endl; return; }//处理凸包退化的场面
int i = 0, j = 0;//某个方向上的接踵点对
//求出x轴方向上的接踵点对,j为最左点,i为最右点
//为什么不用排序取收尾?这样做只需要O(n)
for (ll k = 0; k < n; k++) {
if (!cmp(qs[i], qs[k]))i = k;
if (cmp(qs[j], qs[k]))j = k;
}
double res = 0;
ll si = i, sj = j;
//i--j,如果i转到了sj的位置,那么i转了180了,同理j转到si的位置也就180了
while (i != sj || j != si) {//将方向逐步旋转180,注意条件
res = max(res, dist(qs[i], qs[j]));
// 判断先转到边i-(i+l)的法线方向还是边j-(j+l)的法线方向
if ((qs[(i + 1) % n] - qs[i]).mul(qs[(j + 1) % n] - qs[j]) < 0)
i = (i + 1) % n;// 先转到边i-(i+l)的法线方向
else
j = (j + 1) % n;// 先转到边j-(j+l)的法线方向
}
ll r = res;
cout << r << endl;
}