平面最远点对

平面最近点对可以用分治法在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("请输入所有点的坐标:\n");
    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("\n\n构成凸包的点有:\n");
    for(i=1;i<=g_result[0][0];i++)
        printf("(%d,%d)\n",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

转载于:https://www.cnblogs.com/weiyinfu/p/10659785.html

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值