平面最近点对可以用分治法在O(n*log(n))
时间内解决,平面最远点对可以用凸包+旋转卡壳在O(n*log(n))
时间内解决。
不管是平面最近点对还是平面最远点对,问题都可以避免O(n*n)
。但这两种算法都不能直接应用在高维空间。
POJ2187
题目链接:POJ2187
这道题不使用旋转卡壳,在求完凸包之后直接暴力也是可以通过的。
#include <math.h>
#include <stdio.h>
#include <time.h>
#include <algorithm>
#include <iostream>
#include<stdlib.h>
using namespace std;
const int MAXN = 5e4;
struct Point {
int x, y;
} a[MAXN];
Point *b[MAXN]; //凸包上的顶点
int N; //点的总数
int bi = 0; //凸包顶点的个数
int dis(const Point&p, const Point&q) {
int dx = p.x - q.x, dy = p.y - q.y;
return dx * dx + dy * dy;
}
int cross(const Point &x, const Point &y, const Point & z) {
//向量xy和向量xz之间的关系,若cross大于0,xz在xy的上方,若小于0,则xz在xy的下方
return (x.x - y.x) * (x.y - z.y) - (x.y - y.y) * (x.x - z.x);
}
bool comp(const Point &p, const Point &q) {
//比较两个点到目标点的角度,如果角度相同,让距离近的点排在前面
int c = cross(a[0], p, q);
if (c == 0) {
return dis(a[0], p) - dis(a[0], q) < 0;
}
else {
return c > 0;
}
}
void findBag() {
//寻找凸包
//首先找到最左方、最下方的点
for (int i = 1; i < N; i++) {
if (a[i].x < a[0].x || (a[i].x == a[0].x && a[i].y < a[0].y)) {
swap(a[i], a[0]);
}
}
sort(a + 1, a + N, comp);
b[0] = &a[0];
b[1] = &a[1];
bi = 2;
for (int i = 2; i < N; i++) {
while (bi > 1 && cross(*b[bi - 2], *b[bi - 1], a[i]) <= 0) {
bi--;
}
b[bi++] = &a[i];
}
}
int main() {
freopen("in.txt", "r", stdin);
cin >> N;
for (int i = 0; i < N; i++) {
scanf("%d%d", &a[i].x, &a[i].y);
}
if (N == 1) {
cout << 0 << endl;
return 0;
}
else if (N == 2) {
cout << dis(a[0], a[1]) << endl;
return 0;
}
findBag();
int ans = 0;
for (int i = 0; i < bi; i++) {
for (int j = i + 1; j < bi; j++) {
ans = max(ans, dis(*b[i], *b[j]));
}
}
cout << ans << endl;
return 0;
}
凸包问题的五种解法
暴力算法
对于任意一对顶点A,B,需要O(n)复杂度验证其余顶点是否在它的同一侧,若是,则A、B是凸包的两个顶点,AB构成凸多边形的一条边。整个算法复杂度为O(n^3)
分治法
首先找到最左最下点A、最右最上点B,AB两点必为凸包上的两点,并且AB把整个凸包一分为二,称上下两个凸包为:上凸包和下凸包。接下来在上凸包和下凸包中寻找凸包点。
在上凸包中,找到离直线A、B最远的那个点,记为C,此点必然也是凸包顶点。直线AC上方的点成为左凸包,直线BC上方的点称为右凸包,继续分治即可找到全部顶点。
复杂度为O(n*log(n))
Graham扫描法
首先找到平面中的最左、最下点A,然后将其余点进行排序,排序的标准就是A点和该点的连线的斜率。
对排序之后的点使用一个栈进行遍历,栈顶两点的连线成为栈顶向量。坚守一个法则:即将入栈的顶点一定在栈顶向量的上方。如果不满足此法则,就要弹出栈顶元素。
最终得到的栈中元素就是凸包的顶点。
Grapham算法中遍历顶点复杂度为O(n),主要复杂度集中在顶点排序上,复杂度为O(n*log(n))。
Jarvis步进法
时间复杂度:O(nH)。(其中 n 是点的总个数,H 是凸包上的点的个数)
思路:
纵坐标最小的那个点一定是凸包上的点,例如图上的 P0。
从 P0 开始,按逆时针的方向,逐个找凸包上的点,每前进一步找到一个点,所以叫作步进法。
怎么找下一个点呢?利用夹角。假设现在已经找到 {P0,P1,P2} 了,要找下一个点:剩下的点分别和 P2 组成向量,设这个向量与向量P1P2的夹角为 β 。当 β 最小时就是所要求的下一个点了,此处为 P3 。
Melkman算法
Melkman算法又称“快包”算法,它是一种在线算法。
1887年,Melkman的凸包算法不再像Graham算法那样使用栈实现,而是使用双向链表,使得新加入的顶点既可以放到左边又可以放到右边,这样就一举避免了O(nlog(n))的排序操作,复杂度直接降为O(n)了。并且,此算法是在线的。
一般情况下,在线算法比离线算法复杂度要高,因为在线算法的条件更加苛刻。但Melkman算法既是在线算法复杂度又低于以往的凸包算法。
#include<stdio.h>
#include<stdlib.h>
int g_result[240][2];
/*getResult()实现功能:以坐标P0(x1,y1)和Pn(x2,y2)为直线,找出pack里面里这条直线最远的点Pmax
*并找出直线P0Pmax和PmaxPn的上包,进行递归。
*注:Pack[0][0]存放点的个数,pack[1]开始存放点的坐标。
*全局变量g_result[][]用来存放凸包上的点,即最终所要的答案。同样g_result[0][0]存放的是已找到的点的个数。
**/
void getResult(int Pack[240][2], int x1, int y1, int x2, int y2)
{
int i,t,x3,y3,R,Rmax,tmax;
int ResultPack[240][2];
ResultPack[0][0] = 0;
if(Pack[0][0] <= 1)
return;
x3 = Pack[1][0];
y3 = Pack[1][1];
R = x1*y2 + x3*y1 + x2*y3 - x3*y2 - x2*y1 - x1*y3;
Rmax = R;
tmax = 1;
for(i=2;i<=Pack[0][0];i++)
{
x3 = Pack[i][0];
y3 = Pack[i][1];
R = x1*y2 + x3*y1 + x2*y3 - x3*y2 - x2*y1 - x1*y3;
if(R >= 0)
{
t = ++ResultPack[0][0];
ResultPack[t][0] = x3;
ResultPack[t][1] = y3;
}
if(R > Rmax)
{
Rmax = R;
tmax = i;
}
}
if(Rmax <= 0)
{
for(i=1;i<ResultPack[0][0];i++)
{
x3 = ResultPack[i][0];
y3 = ResultPack[i][1];
R = x1*y2 + x3*y1 + x2*y3 - x3*y2 - x2*y1 - x1*y3;
if(R == 0 && !((x3==x2&&y3==y2)||(x3==x1&&y3==y1)))
{
t = ++g_result[0][0];
g_result[t][0] = ResultPack[i][0];
g_result[t][1] = ResultPack[i][1];
}
}
return;
}
else
{
t = ++g_result[0][0];
g_result[t][0] = Pack[tmax][0];
g_result[t][1] = Pack[tmax][1];
if(ResultPack[0][0] == 0)
return;
}
getResult(ResultPack,x1,y1,Pack[tmax][0],Pack[tmax][1]);
getResult(ResultPack,Pack[tmax][0],Pack[tmax][1],x2,y2);
}
void main()
{
int Point[240][2];//Point存所有点。
int i=1;
int x1,y1,x2,y2,x3,y3;
g_result[0][0]=0;Point[0][0]=0;//Point的第一行第一列元素存放包里面有几个点。初始化为0。
printf("请输入所有点的坐标:
");
while(scanf("%d,%d",&Point[i][0],&Point[i][1]) != EOF)
i++;
Point[0][0] = i-1;
x1 = Point[1][0];
y1 = Point[1][1];
x2 = x1;
y2 = y1;
for(i=2;i<=Point[0][0];i++)
{
x3 = Point[i][0];
y3 = Point[i][1];
if(x3 < x1)
{
x1 = x3;
y1 = y3;
}
else if(x3 > x2)
{
x2 = x3;
y2 = y3;
}
}
g_result[1][0] = x1;
g_result[1][1] = y1;
g_result[2][0] = x2;
g_result[2][1] = y2;
g_result[0][0] += 2;
getResult(Point, x1, y1, x2, y2);
getResult(Point, x2, y2, x1, y1);
printf("
构成凸包的点有:
");
for(i=1;i<=g_result[0][0];i++)
printf("(%d,%d)
",g_result[i][0],g_result[i][1]);
system("pause");
}
旋转卡壳算法
在得到凸包以后,可以只在顶点上面找最远点了。同样,如果不O(n^2)两两枚举,可以想象有两条平行线, “卡”住这个凸包,然后卡紧的情况下旋转一圈,肯定就能找到凸包直径,也就找到了最远的点对。或许这就是为啥叫“旋转卡壳法”。
枚举凸包上的所有边,对每一条边找出凸包上离该边最远的顶点,计算这个顶点到该边两个端点的距离,并记录最大的值。直观上这是一个O(n2)的算法,和直接枚举任意两个顶点一样了。但是注意到当我们逆时针枚举边的时候,最远点的变化也是逆时针的,这样就可以不用从头计算最远点,而可以紧接着上一次的最远点继续计算,于是我们得到了O(n)的算法。
参考资料
凸包问题的五种解法
http://www.cnblogs.com/jbelial/archive/2011/08/05/2128625.html