POJ 2187 Beauty Contest【旋转卡壳求凸包直径】

链接:

http://poj.org/problem?id=2187

http://acm.hust.edu.cn/vjudge/contest/view.action?cid=22013#problem/E

Beauty Contest
Time Limit: 3000MS   Memory Limit: 65536K
Total Submissions: 24254   Accepted: 7403

Description

Bessie, Farmer John's prize cow, has just won first place in a bovine beauty contest, earning the title 'Miss Cow World'. As a result, Bessie will make a tour of N (2 <= N <= 50,000) farms around the world in order to spread goodwill between farmers and their cows. For simplicity, the world will be represented as a two-dimensional plane, where each farm is located at a pair of integer coordinates (x,y), each having a value in the range -10,000 ... 10,000. No two farms share the same pair of coordinates. 

Even though Bessie travels directly in a straight line between pairs of farms, the distance between some farms can be quite large, so she wants to bring a suitcase full of hay with her so she has enough food to eat on each leg of her journey. Since Bessie refills her suitcase at every farm she visits, she wants to determine the maximum possible distance she might need to travel so she knows the size of suitcase she must bring.Help Bessie by computing the maximum distance among all pairs of farms. 

Input

* Line 1: A single integer, N 

* Lines 2..N+1: Two space-separated integers x and y specifying coordinate of each farm 

Output

* Line 1: A single integer that is the squared distance between the pair of farms that are farthest apart from each other. 

Sample Input

4
0 0
0 1
1 1
1 0

Sample Output

2

Hint

Farm 1 (0, 0) and farm 3 (1, 1) have the longest distance (square root of 2) 

Source


题意:

给你 N 个点, 求所有点中最远两点距离

算法:

code1:凸包+暴力
code2:凸包+旋转卡壳

建立凸包:

lrj 《训练指南》 P272

对于个点按照 x 从小到大排序,再按照 y 点从小到大排序,删除重复的点后,得到序列 p0,p1,p2...,
把 p0 和 p1 放入凸包。 从p2开始,当新点在凸包“前进”方向的左边时继续,否则依次删除最近加入凸包的点,直到新点在左边
PS:判断用叉积即可

凸包模板:

/********************************************
计算凸包,输入点数组 p, 个数为 n , 输出点数组 ch. 函数返回凸包顶点数
输入不能有重复点。函数执行完之后输入点的顺序被破坏
如果不希望在凸包的边上有输入点,把两个 <= 改成 < 【== 表示两向量共线】
在精度要求高时建议用 dcmp 比较

const double eps = 1e-10;
int dcmp(double x)
{
    if(fabs(x) < eps) return 0;
    else return x < 0 ? -1 : 1;
}
********************************************/
double ConvexHull(Point* p, int n, Point* ch) /** 基于水平的Andrew算法求凸包 */
{
    sort(p,p+n,cmp); /**先按照 x 从小到大排序, 再按照 y 从小到大排序*/
    int m = 0;

    for(int i = 0; i < n; i++) /** 从前往后找,求"下凸包" */
    {/**Cross <= 0有等于,去掉 重复的点*/
        while(m > 1 && Cross(ch[m-1]-ch[m-2], p[i]-ch[m-2]) <= 0) m--;
        ch[m++] = p[i];
    }
    int k = m;
    for(int i = n-2; i >= 0; i--) /**从后往前找,求"上凸包" 形成完整的封闭背包*/
    {
        while(m > k && Cross(ch[m-1]-ch[m-2], p[i]-ch[m-2]) <= 0) m--;
        ch[m++] = p[i];
    }
    if(n > 1) m--; /** 起点重复 */
    return m;
}

旋转卡壳模板:

盗版的别人博客的模板,下面会贴上大牛的关于这个算法的分析,一般是能看懂的了,如果不能看懂的,记住就好了

int rotating_calipers(Point *ch, int m) /**旋转卡壳模板*/
{
    int q = 1;
    int ans = 0;
    ch[m] = ch[0]; /**凸包边界处理*/
    for(int i = 0; i < m; i++) /**依次用叉积找出凸包每一条边对应的最高点*/
    {/**同底不同高,每次用面积判断高的大小就可以了*/
        while(Cross(ch[i+1]-ch[i], ch[q+1]-ch[i]) > Cross(ch[i+1]-ch[i], ch[q]-ch[i]))
            q = (q+1)%m;
    /**每条底上有两个点,所以要 max 两次*/
        ans = max(ans, max(squarDist(ch[i], ch[q]), squarDist(ch[i+1], ch[q+1])));
    }
    return ans;/**返回的也就是凸包的直径*/
}


思路:

暴力:最远距离两个点一定在凸包上,建立好背包后,枚举凸包上的点就可以了
对于凸包上的点很多,暴力的话肯定是不行的了,那么就用旋转卡壳,只是名字听着神奇,其实也很好理解了
旋转卡壳:直接套模板,求直径

code1:暴力+凸包【对应于凸包上点比较少】

/********************************************
题意:给你 N 个点, 求所有点中最远两点距离
算法:凸包+暴力
思路:最远距离两个点一定在凸包上,建立好背包后,枚举凸包上的点就可以了
*********************************************/
#include<stdio.h>
#include<math.h>
#include<string.h>
#include<algorithm>
using namespace std;

const int maxn = 50000+10;
int n,m;

struct Point{
    double x,y;
    Point(){};
    Point(double _x, double _y)
    {
        x = _x;
        y = _y;
    }

    Point operator - (const Point & B) const
    {
        return Point(x-B.x, y-B.y);
    }
}p[maxn], ch[maxn];

bool cmp(Point p1, Point p2)
{
    if(p1.x == p2.x) return p1.y < p2.y;
    return p1.x < p2.x;
}

int squarDist(Point A, Point B) /**距离的平方*/
{
    return (A.x-B.x)*(A.x-B.x) + (A.y-B.y)*(A.y-B.y);
}

double Cross(Point A, Point B) /**叉积*/
{
    return A.x*B.y-A.y*B.x;
}

void ConvexHull() /** 求凸包 */
{
    sort(p,p+n,cmp); /**先按照 x 从小到大排序, 再按照 y 从小到大排序*/
    m = 0;

    for(int i = 0; i < n; i++) /** 从前往后找"下凸包" */
    {
        while(m > 1 && Cross(ch[m-1]-ch[m-2], p[i]-ch[m-2]) <= 0) m--;
        ch[m++] = p[i];
    }
    int k = m;
    for(int i = n-2; i >= 0; i--) /**从后往前找"上凸包", 形成完整的封闭背包*/
    {
        while(m > k && Cross(ch[m-1]-ch[m-2], p[i]-ch[m-2]) <= 0) m--;
        ch[m++] = p[i];
    }
    if(n > 1) m--; /** 起点重复*/
}

int main()
{
    while(scanf("%d", &n) != EOF)
    {
        if(n == 0) break;
        for(int i = 0; i < n; i++)
            scanf("%lf%lf", &p[i].x, &p[i].y);

        ConvexHull();
        int ans = 0;
        for(int i = 0; i < m; i++)
            for(int j = i+1; j < m; j++)
                ans = max(ans,squarDist(ch[i], ch[j]));
        printf("%d\n", ans);
    }
    return 0;
}

code2:凸包+旋转卡壳【对于凸包上点比较多】

看了下面的博客,套模板来的了,对于点多时才能体现效率
 /********************************************
2187    Accepted    972K    297MS   C++ 1927B   2013-07-27 13:38:35
题意:给你 N 个点, 求所有点中最远两点距离
算法:凸包+旋转卡壳
思路:最远距离两个点一定在凸包上,建立好背包后,直接套用旋转卡壳找直径
*********************************************/
#include<stdio.h>
#include<math.h>
#include<string.h>
#include<algorithm>
using namespace std;

const int maxn = 50000+10;
int n,m;

struct Point{
    double x,y;
    Point(){};
    Point(double _x, double _y)
    {
        x = _x;
        y = _y;
    }

    Point operator - (const Point & B) const
    {
        return Point(x-B.x, y-B.y);
    }
}p[maxn], ch[maxn];

bool cmp(Point p1, Point p2)
{
    if(p1.x == p2.x) return p1.y < p2.y;
    return p1.x < p2.x;
}

int squarDist(Point A, Point B) /**距离的平方*/
{
    return (A.x-B.x)*(A.x-B.x) + (A.y-B.y)*(A.y-B.y);
}

double Cross(Point A, Point B) /**叉积*/
{
    return A.x*B.y-A.y*B.x;
}

void ConvexHull() /** 基于水平的Andrew算法求凸包 */
{
    sort(p,p+n,cmp); /**先按照 x 从小到大排序, 再按照 y 从小到大排序*/
    m = 0;

    for(int i = 0; i < n; i++) /** 从前往后找 */
    {
        while(m > 1 && Cross(ch[m-1]-ch[m-2], p[i]-ch[m-2]) <= 0) m--;
        ch[m++] = p[i];
    }
    int k = m;
    for(int i = n-2; i >= 0; i--) /**从后往前找, 形成完整的封闭背包*/
    {
        while(m > k && Cross(ch[m-1]-ch[m-2], p[i]-ch[m-2]) <= 0) m--;
        ch[m++] = p[i];
    }
    if(n > 1) m--;
}

int rotating_calipers() /**旋转卡壳模板*/
{
    int q = 1;
    int ans = 0;
    ch[m] = ch[0]; /**凸包边界处理*/
    for(int i = 0; i < m; i++) /**依次用叉积找出凸包每一条边对应的最高点*/
    {
        while(Cross(ch[i+1]-ch[i], ch[q+1]-ch[i]) > Cross(ch[i+1]-ch[i], ch[q]-ch[i]))
            q = (q+1)%m;
        ans = max(ans, max(squarDist(ch[i], ch[q]), squarDist(ch[i+1], ch[q+1])));
    }
    return ans;
}

int main()
{
    while(scanf("%d", &n) != EOF)
    {
        if(n == 0) break;
        for(int i = 0; i < n; i++)
            scanf("%lf%lf", &p[i].x, &p[i].y);

        ConvexHull();

        printf("%d\n", rotating_calipers());
    }
    return 0;
}


 


旋转卡壳算法分析:

转载来自:http://www.cnblogs.com/xdruid/archive/2012/07/01/2572303.html

好早以前看的,现在再记下来吧,当做复习一遍。

那么,先提一下最基本最暴力的求凸包直径的方法吧---枚举。。。好吧。。很多问题都可以用 枚举 这个“万能”的方法来解决,过程很简单方便是肯定的,不过在效率上就要差很远了。  要求一个点集的直径,即使先计算出这个点集的凸包,然后再枚举凸包上的点对,这样来求点集直径的话依然会在凸包上点的数量达到O(n)级别是极大的降低它的效率,也浪费了凸包的优美性质。不过在数据量较小或者很适合时,何必要大费周折的用那些麻烦复杂的算法呢,枚举依然是解决问题的很好的方法之一。

 

然后就是今天的旋转卡壳算法了。(那个字念 qia 。。。搞了好久才发现读都读错了。囧。。。)

旋转卡壳可以用于求凸包的直径、宽度,两个不相交凸包间的最大距离和最小距离等。虽然算法的思想不难理解,但是实现起来真的很容易让人“卡壳”。

POJ 2187 Beauty Contest【旋转卡壳求凸包直径】_第1张图片

其实简单来说就是用一对平行线“卡”住凸包进行旋转。

被一对卡壳正好卡住的对应点对称为对踵点,对锺点的具体定义不好说,不过从图上还是比较好理解的。

可以证明对踵点的个数不超过3N/2个 也就是说对踵点的个数是O(N)

对踵点的个数也是我们下面解决问题时间复杂度的保证。

 

卡壳呢,具体来说有两种情况:

1.

POJ 2187 Beauty Contest【旋转卡壳求凸包直径】_第2张图片一种是这样,两个平行线正好卡着两个点;

2.

 

POJ 2187 Beauty Contest【旋转卡壳求凸包直径】_第3张图片一种是这样,分别卡着一条边和一个点。

而第二种情况在实现中比较容易处理,这里就只研究第二种情况。

 

在第二种情况中 我们可以看到 一个对踵点和对应边之间的距离比其他点要大(借用某大神的图··)

 

也就是一个对踵点和对应边所形成的三角形是最大的 下面我们会据此得到对踵点的简化求法。

下面给出一个官方的说明:

Compute the polygon's extreme points in the y direction. Call them ymin and ymax. Construct two horizontal lines of support through ymin and ymax. Since this is already an anti-podal pair, compute the distance, and keep as maximum. Rotate the lines until one is flush with an edge of the polygon. A new anti-podal pair is determined. Compute the new distance, compare to old maximum, and update if necessary. Repeat steps 3 and 4 until the anti-podal pair considered is (ymin,ymax) again. Output the pair(s) determining the maximum as the diameter pair(s). 

POJ 2187 Beauty Contest【旋转卡壳求凸包直径】_第4张图片

要是真的按这个实现起来就麻烦到吐了。。

根据上面的第二种情况,我们可以得到下面的方法:

如果qa,qb是凸包上最远两点,必然可以分别过qa,qb画出一对平行线。通过旋转这对平行线,我们可以让它和凸包上的一条边重合,如图中蓝色直线,可以注意到,qa是凸包上离p和qb所在直线最远的点。于是我们的思路就是枚举凸包上的所有边,对每一条边找出凸包上离该边最远的顶点,计算这个顶点到该边两个端点的距离,并记录最大的值。

直观上这是一个O(n2)的算法,和直接枚举任意两个顶点一样了。

然而我们可以发现 凸包上的点依次与对应边产生的距离成单峰函数(如下图:)

 

这个性质就很重要啦。

 

根据这个凸包的特性,我们注意到逆时针枚举边的时候,最远点的变化也是逆时针的,这样就可以不用从头计算最远点,而可以紧接着上一次的最远点继续计算。于是我们得到了O(n)的算法。这就是所谓的“旋转”吧!

利用旋转卡壳,我们可以在O(n)的时间内得到凸包的对锺点中的长度最长的点对。

 

又由于最远点对必然属于对踵点对集合 ,那么我们先求出凸包 然后求出对踵点对集合 然后选出距离最大的即可

 

那么具体的代码就很容易实现了,利用叉积,代码只有这么几行的长度:

 

double rotating_calipers(Point *ch,int n) 
{
    int q=1;
    double ans=0;
    ch[n]=ch[0];
    for(int p=0;p<n;p++)
    {
        while(cross(ch[p+1],ch[q+1],ch[p])>cross(ch[p+1],ch[q],ch[p]))
            q=(q+1)%n;
        ans=max(ans,max(dist(ch[p],ch[q]),dist(ch[p+1],ch[q+1])));
     }
    return ans;
}

其中cross()是计算叉积,因为凸包上距离一条边最远的点和这条边的两个端点构成的三角形面积是最大的。之所以既要更新(ch[p],ch[q])又要更新(ch[p+1],ch[q+1])是为了处理凸包上两条边平行的特殊情况。

旋转卡壳经典题目:
求凸包距离: http://poj.org/problem?id=3608


你可能感兴趣的:(POJ 2187 Beauty Contest【旋转卡壳求凸包直径】)