安德鲁算法
安德鲁算法(Andrew’s Algorithm)是计算几何当中一种求凸包的算法。
什么是凸包
在一个实数向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包。X的凸包可以用X内所有点(X1,…Xn)的凸组合来构造。
简单来讲,对于一个二维空间的点集,这个点集当中的一些点总可以形成一个凸多边形,而这个凸多边形之内恰好可以包括除了组成凸包这个凸多边以外的所有点,而这个凸多边形就是凸包。
再形象的理解一下,凸包可以看成是再木板上钉了许多钉子,用一根橡皮经框住所有钉子是所得到的多边形。
如上图,红色点集当中的一些点可以组成黄色的凸多边,而这个凸多边恰好可以囊括所有的红色点集,这个多边形/组成多边形的点集就叫做凸包。
如何构建一个凸包
凸包的特点
首先我们观察一下任何一个凸包,很明显的一个特点就是,凸包是凸多边
那么凸多边有什么用呢,我们来看看一个凸包上连续三点的一些关系。
我们让这连续的三个点分别为p0,p1,p2。
向量 a = p1 - p0;
向量 b = p2 - p0;
不难发现:
p2在向量a的右端
也就是p2在向量a的顺时针方向。
用叉积来表示上面的语句:向量a X(叉乘) 向量b < 0
对于凸包上按照顺时针方向的每三个点,均满足这个规律。读者可自行试试。
如果一个点不在凸包上
倘若一个点不再凸包上,就不会满足这个规律。我们不妨尝试一下:
假如p0,p1为凸包上的点,p2不在凸包上。
对于向量a和向量b,好像没什么问题:p2也在向量a的顺时针方向
但是一但是下一个点呢?
我们先假设这个绿色的点是在凸包内的,对于下一个点,对于这个点和之前凸包内连续的两点(就是上一次的p1和p2)来计算叉积。
这次的p2在由向量a的左侧,叉积值大于0,不满足我们的规律了。
所以一旦一个点不是组成凸包的点,那么经过这个点组成的多边形就不是凸多边形,更不是凸包了。
使用andrew’s algoritihm构造一个凸包的顺序
step0 数据定义/常用方法。
class Point///点的定义。
{
public:
double x,y;
Point(double x = 0,double y = 0):x(x),y(y) {}
Point operator + (Point a)
{
return Point(x + a.x, y + a.y);
}
Point operator - (Point a)
{
return Point(x - a.x, y - a.y);
}
bool operator < (const Point &a) const
{
if(x==a.x) return y < a.y;
return x < a.x;
}
bool operator == (Point a)
{
if(x==a.x && y == a.y) return true;
return false;
}
double abs(void)
{
return sqrt(x*x + y*y);
}
};
typedef vector<Point> Ploygom;
typedef Point Vector;
其中点是一个类,其中double x,y;存储其的坐标,下面重载了一些常用的运算。排序方法也在其中重载<来定义。
而对于Vector,不难理解其就是一个点(向量可以移动,其的起点可以移动到原点,那么向量的表达就和点的表达方法一直)。
对于多边形,我们可以看成其是一系列点的集合,我们使用vector 来存储。
我们设原始的点集为多边形 p。
double cross(Vector a,Vector b)///叉积
{
return a.x*b.y - a.y*b.x;
}
bool isclock(Point p0,Point p1,Point p2)///顺时针方向
{
Vector a = p1 - p0;
Vector b = p2 - p0;
if(cross(a,b) < 0) return true;
return false;
}
step1 排序
对于所有的二维坐标点,按照x从小到大排序,在x相同的情况下,对于y从小到大排序。
sort(p.begin(),p.end());
step2 创建凸包上部
定义凸包上部为 ploygom u
首先先加入排序完的点集当中的头两个。
u.push_back(p[0]);
u.push_back(p[1]);
对于每一个点,我们取出当前以构建凸包内的最末尾的两个点,判断这个点是否与前两个点是否可以继续构造凸多边形。
如果是顺时针构造凸包,那就是这个点是否在前两个点组成的向量的顺时针方向。
如果是:将当前点加入凸包
如果不是:回溯,删除凸包内最后一个点,同时再取出删除后的凸包的最后两个点,再进行判断。直到不断删除之后凸包内只剩下一个点了 或者 当前点与最后两个点可以组成凸多边形。
for(int i = 2 ; i < p.size() ; i++)
{
for(int n = u.size() ; n >= 2 && isclock(u[n-2],u[n-1],p[i])!=true ; n--)
u.pop_back();
u.push_back(p[i]);
}
- 一开始i=2是因为最开始的两个点已经加入了凸包,我们从点集的第三个点开始。
- 第二个for循环就是不断判断是否可以将这个点加入,如果可以加入,那么isclock(u[n-2],u[n-1],p[i])的结果就是true,之后就会跳出第二个for循环,从而将这个点加入凸包上部的集合;如果不能加入,则一直会删除已经加入凸包当中的最后一个点,直到最后两个点和这个点可以组成凸多边或者凸包内只剩下一个点。
凸包上部加到第6个点的时候都好好的….但是加到第七个点的时候…..
那就删掉第六个点,但是这样还是不够。
那只有一直删掉凸包内最后一个点,不断判断,直到满足其是一个凸多边形为止。
如此,一直处理完所有的点。得到凸包上部u。
创建凸包下部
原理与创建凸包上部相同,因为是顺时针创建凸包,所以对于所有点需要从后向前向前存储。
同样的,一开始储存两个点。
l.push_back(s[s.size() - 1]);
l.push_back(s[s.size() - 2]);
然后按照创建凸包上部的方法创建凸包。
for(int i = s.size() - 3 ; i >= 0 ; i--)
{
cout<<i<<endl;
for(int n = l.size() ; n >=2 && isclock(l[n-2],l[n-1],s[i])!=true ; n--)
{
cout<<i<<endl;
printf("del:\nl[n-2]:%.2f %.2f\nl[n-1]:%.2f %.2f\n",l[n-2].x,l[n-2].y,l[n-1].x,l[n-1].y);
l.pop_back();
}
l.push_back(s[i]);
}
之后得到凸包下部l
连接凸包上部和下部,得到凸包点集
我们完成了凸包的上部和下部,然后我们将其按照一定顺序连接即可。
需要注意的是,无论是凸包上部还是凸包下部,其的起点和终点的点必然重合。
很明显,起点和终点是重合的,我们在合并凸包上部和下部的时候只用保留其中一个的起点和终点即可。
这里给出按照顺时针合并的凸包合并代码。
for(int i = 1 ; i < u.size() - 1 ; i++) l.push_back(u[i]);
return l;
将凸包上部从第二个点开始到倒数n-1个点加入了凸包下部,之后l就是一个完整的凸包。返回即可。
完整代码
主函数依照题目而定,这里不给出主函数。
#include <bits/stdc++.h>
using namespace std;
#define eps 0.000001
class Point
{
public:
double x, y;
Point(double x = 0, double y = 0):x(x),y(y) {}
Point operator + (Point a)
{
return Point(a.x + x,a.y + y);
}
Point operator - (Point a)
{
return Point(x - a.x, y - a.y);
}
bool operator < (const Point &a) const
{
if(x ==a.x)
return y < a.y;
return x < a.x;
}
};
typedef Point Vector;
double cross(Vector a,Vector b)
{
return a.x*b.y - a.y*b.x;
}
double dot(Vector a,Vector b)
{
return a.x* b.x + a.y*b.y;
}
bool isclock(Point p0,Point p1,Point p2)
{
Vector a = p1 - p0;
Vector b = p2 - p0;
if(cross(a,b) < -eps) return true;
return false;
}
double getDistance(Point a,Point b)
{
return sqrt(pow(a.x-b.x,2)+pow(a.y - b.y,2));
}
typedef vector<Point> Polygon;
Polygon andrewScan(Polygon s)
{
Polygon u,l;
if(s.size()<3) return s;
sort(s.begin(),s.end());
u.push_back(s[0]);
u.push_back(s[1]);
l.push_back(s[s.size() - 1]);
l.push_back(s[s.size() - 2]);
printf("l[n-2]:%.2f %.2f\nl[n-1]:%.2f %.2f\n",l[l.size()-2].x,l[l.size()-2].y,l[l.size()-1].x,l[l.size()-1].y);
for(int i = 2 ; i < s.size() ; i++)
{
for(int n = u.size() ; n >= 2 && isclock(u[n-2],u[n-1],s[i])!=true; n--)
{
cout<<u[n-2].x<<' '<<u[n-2].y<<'\n'<<u[n-1].x<<u[n-1].y<<endl;
u.pop_back();
}
u.push_back(s[i]);
}
for(int i = s.size() - 3 ; i >= 0 ; i--)
{
cout<<i<<endl;
for(int n = l.size() ; n >=2 && isclock(l[n-2],l[n-1],s[i])!=true ; n--)
{
cout<<i<<endl;
printf("del:\nl[n-2]:%.2f %.2f\nl[n-1]:%.2f %.2f\n",l[n-2].x,l[n-2].y,l[n-1].x,l[n-1].y);
l.pop_back();
}
l.push_back(s[i]);
}
// for(auto &p : u) printf("%.2f %.2f\n",p.x,p.y);
// printf("yes\n");
// for(auto &p : l) printf("%.2f %.2f\n",p.x,p.y);
for(int i = 1 ; i < u.size() - 1 ; i++) l.push_back(u[i]);
return l;
}
来做几道题目吧
POJ 3348 Cows
一道凸包裸题,最后用叉积计算一下凸多边形的面积即可。
题解:点击进入
HDU 1932 Surround the Trees
凸包大裸题,最后计算一下每个凸包的两点距离即可
题解:点击进入
POJ 1113 Wall
凸包+一点点思维
题解:点击进入
POJ 1228 Grandpa’s Estate
凸包+一点点思维。题意不好想,判断当前凸包是否可能是由更大的凸包删除而来,代码很简单。
题解:点击进入
总结
安德鲁算法算的实现和思维都比较好想,而且理论上时间复杂度是线性的(On),因为有一个排序实际算法的时间复杂度是nlogn。
凸包在比赛当中出现的情况还算比较常见,但是一般很难使用到…一般做到计算几何之后都是冲金牌的题目,而且很多题目如果不是板子题很难想到使用凸包。
最后谢谢大家食用本文!