計算幾何及其應用——凸包問題


   其實與計算幾何中的最小圓覆蓋問題很類似,凸包問題探究的是如何構造可以覆蓋給定點集最小的凸多邊形。
   我們先從人腦的思維來分析一下這個問題,所謂凸包,起名字包含了兩個關鍵的信息。
  1.凸:這里所求作的是凸多邊形,這是很關鍵的一點。因為在構造的時候可能會有下圖的疑問。

 
  右邊的圖的面積豈不是更小?但是我們還是要構造出左邊的圖,因為題設給出了——是凸多邊形。

  2.包(這里談Jarvis算法):這個字的含義在於這個凸多邊形必須要把給定的點集都給包住,這個條件使我們思考如何構造這個凸多邊形的開始。既然想包住,我們顯然要從點集中最靠外圍的點開始構造(這顯而易見),這里我們從最靠左下的點A開始。因為這個點已經是最下面的了,所以我們做出一條以A為端點的水平線射線,在這條射線下是不可能有點的, 我們開始在[0,180)旋轉這個射線來“掃描”符合要求的點,最先出現的點(設為B)便是我們想要的,於是我們再以這個點為端點再做一條射線,開始循環操作。這里在掃描點的時候,無非會出現下面兩種情況:
  
  可以看到,在定量描述A、B、C的相對位置的時候,我們引入的向量及其叉積。因為在一般情況下我們都會知道點的坐標。由此我們就有了“選點”的依據。在這種情況下,我們可以保證選出來的點構成的凸多邊形一定能包住所有的點。 

有了上面對凸包概念更深層次的理解,我們來結合一個問題更深入的探討凸包問題的應用。
 
  閱讀完此題后發現,這里其實是一個間接應用凸包的一個問題,關鍵就在於題設中說圍牆必須離城堡不少於L。我們進行簡單地作圖會得到下面的圖形。

 
   我們以每個節點為圓心,L為半徑做出了n個圓,然后將凸包的各個邊進行平移,便會得到外圍帶有圓弧的圖形。上圖形成了一個類環圖形。我們這里采用高等數學中極限的思想進行理解:內環和外環都有無數個點,並且他們一一對應,而且對應點之間的距離都是L。如果某組對應點之間的距離大於L,那么在外環中那個點的對應位置會形成一個小小的"凸起",顯然周長就增大了。因此這種方式得到的圍牆周長一定是最小的。以上是對“最小周長”的一個簡單的證明。
  有了這層證明,我們會發現,外圍的周長其實是凸包的周長加上以L為半徑的周長,這就把整個問題轉化到了凸包問題上來。

  構造凸包的算法實現:
  1.上文提及構造凸包需要從最外圍的點開始選,因此在輸入點之后進行排序是必不可少的。
  2.有了上述關於選點的叉積判別式,不難從點集中篩選出凸包的各個頂點。
  
  代碼如下。

#include<cmath>

#include<algorithm>

#include<stdio.h>

#define size 1000

using namespace std;

struct pint

{

int x,y;

}x[size];

 

int n,l,ans[size],cnt,sta[size],tail;

 

bool cmp(pint a,pint b)

{

return (a.y < b.y || (a.y==b.y && a.x<b.x));       //對點集進行排序

}

bool CrossLeft(pint p1,pint p2,pint p3)

{

//比較是否是左轉,如果共線不算是左轉

return ((p3.x-p1.x)*(p2.y-p1.y)-(p2.x-p1.x)*(p3.y-p1.y))<0; //選點

}

void make_Convex_hull()

{

tail = cnt = 0;

sort(x,x + n,cmp);

sta[tail++] = 0;

sta[tail++] = 1;

for(int i = 2;i < n;i++)                        //對排序后的點集進行遍歷

{

while(tail > 1 && !CrossLeft(x[sta[tail-1]] , x[sta[tail-2]] , x[i]))  

tail--;

sta[tail++]=i;                        //這一層的循環會在點集的最高點處結束,此時凸包構造了一半

}

for(int i = 0;i < tail;i++)               //記錄篩選出來的點

ans[cnt++] = sta[i];

 

tail = 0;

sta[tail++] = n - 1;

sta[tail++] = n - 2;

for(int i = n-3 ;i >= 0;i--)             //從最高點開始繼續進行構造

{

while(tail>1 && !CrossLeft(x[sta[tail-1]],x[sta[tail-2]],x[i]))

tail--; 

sta[tail++]=i;                       //記錄篩選出來的點

}

for(int i=0;i<tail;i++)

ans[cnt++]=sta[i];

}

int main()

{

int t;

while(scanf("%d",&t)!=EOF)

{

while(t--)

{

scanf("%d%d",&n,&l);

 

for(int i = 0;i < n;i++)

scanf("%d%d",&x[i].x , &x[i].y);

 

make_Convex_hull();

double re=4 * acos(0.0) * l;

for(int i = 0;i < cnt - 1;i++)

re += sqrt((x[ans[i]].x - x[ans[i + 1]].x) *( x[ans[i]].x - x[ans[i + 1]].x)

 +(x[ans[i]].y - x[ans[i + 1]].y) * (x[ans[i]].y - x[ans[i + 1]].y));

printf("%.0lf\n",re);

if(t)  printf("\n");

 

}

}

return 0;

}

 


  然我們再看一道用Jarvis算法實現的凸包問題。
 
  參考代碼。
   

 #include<iostream>
#include<algorithm>
#include<stdio.h>
#include<cmath>

using namespace std;

const int Max = 105;

struct Point
{
      double x , y;
}p[Max];

int n ,res[Max],top;

bool cmp(Point a,Point b)
{
    if(a.y == b.y)  return a.x < b.x;
    return a.y < b.y;
}
bool mult(Point sp , Point ep , Point op)
{
    return(sp.x - op.x)*(ep.y-op.y) >= (ep.x-op.x)*(sp.y-op.y);
}
void Graham()
{
    int i , len;
    top = 1;
    sort(p,p+n,cmp);
    if(n == 0)  return; res[0] = 0;
    if(n == 1)  return; res[1] = 1;
    if(n == 2)  return; res[2] = 2;
    for(i = 2;i < n;i++)
    {
        while(top && mult(p[i],p[res[top]],p[res[top-1]]))  top--;
        res[++top] = i;
    }
    len = top;
    res[++top] = n - 2;
       for(i = n - 3;i >= 0;i--)
       {
             while(top != len && mult(p[i],p[res[top]],p[res[top-1]]))  top--;
             res[++top] = i;
       }
}

 


 今天來討論構建凸包的另一個算法——Graham算法。
  相對Jarvis算法選取先選最下面的點,這里Graham算法先選取最左端的點,然后對點集進行極角排序(Jarvis算法按照縱坐標大小來排序,而Graham算法是按照該點與極點連線的斜率進行排序。),然后模擬棧的機理,起始的時候在棧中放入兩個點,然后加入下一個點,判斷是否滿足左轉(否則無法形成凸角),不滿足的話剔除掉棧頂端的點,然后加入下一個點。遍歷點集之后,此時棧中的頂部的點一定是點集中最靠上的點,然后再設置一次遍歷,完成對凸包的構造。
  參考下圖可以更好地理解這個算法的過程。




    我們來通過一個具體的凸包問題來體會一下這種算法的編程實現。(Problem source : hdu1392)
 
  

  題目大意:給你一些樹的點,現在需要用一條管道把所有的數都圈起來,這里讓你計算需要多長的管道。
  編程實現:我們可以看到,這是很明顯的凸包問題。而這里我們考慮有Graham算法來實現它,其編程實現的一個重點就是極角排序和模擬棧選凸包點的過程,完成了這兩個重要的過程,所求的凸包就記錄在我們開的棧空間中,然后就不難求出周長了。
  極角排序:根據其定義,我們可以輕松的將其轉化成叉積的形式。
 
  參考代碼如下。
 

 #include<stdio.h>
#include<algorithm>
#include<cmath>
using namespace std;

const int maxn = 110;
const double eps = 1e-8;
int n , stack[maxn];
struct point
{
     double x , y;
} p[maxn];

double dis(point a , point b)
{
     return sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y));
}
double Cross_left(point a , point b , point c)
{
    return (a.x - c.x)*(b.y - c.y) - (a.y - c.y)*(b.x - c.x);
}
bool cmp(point a , point b) //極角排序
{
    double x = Cross_left(a , b , p[0]);
    if(x > 0)    return true;
    else if(fabs(x) <= eps && (dis(b,p[0]) >= dis(a,p[0])))  return true;
    else   return false;
}

double Graham()
{
     int i , k = 0 , top = 2;
     point pmin=p[0] ;
     for(i = 1;i < n;i++)
     {
          if(p[i].y < pmin.y || (p[i].y - pmin.y <= eps && p[i].x < pmin.x))
          {
               pmin = p[i] , k = i;  //找極點
          }
     }
     swap(p[0] , p[k]);
     sort(p+1,p+n,cmp);
     for(i = 0;i < 3;i++) stack[i] = i;

      for(i = 3;i < n;i++)
      {
           while(Cross_left(p[stack[top]] , p[i] , p[stack[top-1]]) <= 0)//共線情況不入棧
           {
                top--;
                if(top == 0)  break;
           }
           stack[++top] = i;
      }
      double cir = 0;top++;
      for(i = 0;i < top;i++)
          cir += dis(p[stack[i]] , p[stack[(i+1)%top]]);

      return cir;

}
int main()
{
    while(scanf("%d",&n)!= EOF && n)
    {
         for(int i = 0;i < n;i++)
              scanf("%lf%lf",&p[i].x,&p[i].y);
         if(n == 1)  printf("0.00\n");
         else if(n == 2) printf("%.2lf\n",dis(p[0],p[1]));
         else
            printf("%.2lf\n",Graham());
    }
}

  基於我們對凸包問題的探討,我們再來看一道與凸包問題有關的題目。(Problem source : hdu 4978)
 
  題目大意:平面內有無數條間隔為D的平行線,現在有一枚直徑為D的硬幣,內有n個點,並且滿足任意三個點不在一條直線,任意兩點之間有連線的條件,現在問你拋擲這枚硬幣,硬幣內的線段與平行線相交的概率是多少。
  數理分析:我們這里首先給出一個概率論的模型。
  蒲豐拋針實驗:將長度為L的針拋入間距為D(L<D)的平行線中,針與平行線相交的概率P = 2L/πD。
  這是概率論中求π的近似值的一個經典幾何概型,在以后討論概率論的文章中我們會詳細的介紹,這里不展開這個模型的證明。
  在題目描述中給出的“直徑為D內”的限定,其實就呼應了蒲豐拋針實驗中L < D的限制條件,並且我們需要把n個點構成的圖形看做一個整體,顯然我們需要構造一個凸包(因為它呈現出這個整體的邊界,我們只需討論邊界與平行線相交的概率即可)。
  這樣,我們就把蒲豐拋針和凸包兩個模型結合在了一起,但是在具體求解的過程中,我們應該怎樣操作呢?
  假設我們已經做出了包含n個頂點的凸包,第i條邊的邊長為Li,那么單獨來看這條邊,它與平行線相交的概率Pi = (2 Li) / (πD),考慮到每條直線與改邊相交的同時,一定會和該凸包的第j條邊相交,因此我們遍歷每一條邊的時候,應該乘上1/2,即P = 1/2∑Pi = L/πD,這里的L,即是凸包的周長。
  有了這層數理邏輯,我們只需通過我們學習過的Graham-scan算法構造出凸包並求出周長,然后帶入上面的式子即可。
  參考代碼如下。
 

 #include<stdio.h>
#include<algorithm>
#include<cmath>
using namespace std;

const double pi = acos(-1.0);
const int maxn = 110;
const double eps = 1e-8;
int n , stack[maxn];
struct point
{
     double x , y;
} p[maxn];

double dis(point a , point b)
{
     return sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y));
}
double Cross_left(point a , point b , point c)
{
    return (a.x - c.x)*(b.y - c.y) - (a.y - c.y)*(b.x - c.x);
}
bool cmp(point a , point b) //極角排序
{
    double x = Cross_left(a , b , p[0]);
    if(x > 0)    return true;
    else if(fabs(x) <= eps && (dis(b,p[0]) >= dis(a,p[0])))  return true;
    else   return false;
}

double Graham()
{
     int i , k = 0 , top = 2;
     point pmin=p[0] ;
     for(i = 1;i < n;i++)
     {
          if(p[i].y < pmin.y || (p[i].y - pmin.y <= eps && p[i].x < pmin.x))
          {
               pmin = p[i] , k = i;  //找極點
          }
     }
     swap(p[0] , p[k]);
     sort(p+1,p+n,cmp);
     for(i = 0;i < 3;i++) stack[i] = i;

      for(i = 3;i < n;i++)
      {
           while(Cross_left(p[stack[top]] , p[i] , p[stack[top-1]]) <= 0)//共線情況不入棧
           {
                top--;
                if(top == 0)  break;
           }
           stack[++top] = i;
      }
      double cir = 0;top++;
      for(i = 0;i < top;i++)
          cir += dis(p[stack[i]] , p[stack[(i+1)%top]]);

      return cir;

}
int main()
{
  int ncases;
  int t = 1;
  double d;
  scanf("%d",&ncases);
  while(ncases--)
  {
        scanf("%d%lf",&n,&d);
        for(int i = 0;i < n;i++)
              scanf("%lf%lf",&p[i].x,&p[i].y);
        printf("Case #%d: ",t++);
        if(n == 1)  printf("0.0000\n");
        else if(n == 2) printf("%.4lf\n",2*dis(p[0],p[1])/(pi*d));
        else
            printf("%.4lf\n",Graham()/(pi*d));

  }
}

  

 

  基於我們對凸包問題的探討,我們現在思考這樣一個軍事問題,在領土內,如何設置一個放哨點,使其可以看到領土的每一個角落?
  抽象化得看這個模型的話,就是求一個平面幾何圖形的內核。而內核是什么呢?即該點與多邊形上的任意一個點的連線都在該幾何圖形的內部,想想看,內核是否就是上面那個軍事問題中符合要求的放哨點?
  而如何用計算機設計算法來找到這個內核呢,或者說內核區域呢?這里我們就要用到半平面求交。
  關於半平面求交的定義很簡單,即給出一條直線ax+by+c = 0,和一個平面a。我們取ax + by + c > 0 (當然也可以小於零)並且屬於a的平面區域a'。
  那么我們回到這個求解內核的實際問題當中來,我們得到順指針排列的多邊形的點集V。
  基於內核的定義,需要點與邊上的任意點的連線都在多邊形內部,那么我們考慮依次遍歷所有的邊。然后以改邊所在直線為准,進行半平面求交,這里計算ax+by+c大於0還是小於0是根據第一個邊來確定的。經過半平面求交計算求得的平面區域,表示該區域的點與當前遍歷的邊上任意點的連線是在多邊形內部的(凹角區域除外,但遍歷到凹角所在邊時可排除),然后遍歷多邊形的邊,再連續半平面求交,因此,當我們遍歷了所有的邊的時候,剩下的平面區域滿足與所有邊上的任意點的連線都在多邊形內部,即是所謂的內核區域。
  因此我們可以概括出如下的算法步驟:
  step1:得到多邊形頂點集V,元素按照順時針或者逆時針排列。
  step2:i = 1,得到邊e1 = v1v2,計算e1所在直線l1:a1x + b1y + c1 = 0 ,計算l[1]與多邊形半平面的交(並確定直線取大於0還是小於0),得到平面flat[2].
  step3:i∈[2,n],遍歷i,得到邊ei = viv(i+1),計算其所在直線l[i]:ai*x + bi*y + c = 0,計算li與平面flat[i]半平面的交。
  最終flat[n]即是內核區域。
  我們通過一個具體的題目來實現以下上述的算法過程。(pku 3335)
 

Description

This year, ACM/ICPC World finals will be held in a hall in form of a simple polygon. The coaches and spectators are seated along the edges of the polygon. We want to place a rotating scoreboard somewhere in the hall such that a spectator sitting anywhere on the boundary of the hall can view the scoreboard (i.e., his line of sight is not blocked by a wall). Note that if the line of sight of a spectator is tangent to the polygon boundary (either in a vertex or in an edge), he can still view the scoreboard. You may view spectator's seats as points along the boundary of the simple polygon, and consider the scoreboard as a point as well. Your program is given the corners of the hall (the vertices of the polygon), and must check if there is a location for the scoreboard (a point inside the polygon) such that the scoreboard can be viewed from any point on the edges of the polygon.

Input

The first number in the input line, T is the number of test cases. Each test case is specified on a single line of input in the form n x1 y1 x2 y2 ... xn yn where n (3 ≤ n ≤ 100) is the number of vertices in the polygon, and the pair of integers xi yi sequence specify the vertices of the polygon sorted in order.

Output

The output contains T lines, each corresponding to an input test case in that order. The output line contains either YES or NO depending on whether the scoreboard can be placed inside the hall conforming to the problem conditions.


  參考代碼如下。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;

const double eps = 1e-8;
const int maxn = 105;

int dq[maxn], top, bot, pn, order[maxn], ln;
struct Point {
    double x, y;
} p[maxn];

struct Line {
    Point a, b;
    double angle;
} l[maxn];

int dblcmp(double k) {
    if (fabs(k) < eps) return 0;
    return k > 0 ? 1 : -1;
}

double multi(Point p0, Point p1, Point p2) {
    return (p1.x-p0.x)*(p2.y-p0.y)-(p1.y-p0.y)*(p2.x-p0.x);
}

bool cmp(int u, int v) {
    int d = dblcmp(l[u].angle-l[v].angle);
    if (!d) return dblcmp(multi(l[u].a, l[v].a, l[v].b)) < 0;
    return d < 0;
}

void getIntersect(Line l1, Line l2, Point& p) {
    double dot1,dot2;
    dot1 = multi(l2.a, l1.b, l1.a);
    dot2 = multi(l1.b, l2.b, l1.a);
    p.x = (l2.a.x * dot2 + l2.b.x * dot1) / (dot2 + dot1);
    p.y = (l2.a.y * dot2 + l2.b.y * dot1) / (dot2 + dot1);
}

bool judge(Line l0, Line l1, Line l2) {
    Point p;
    getIntersect(l1, l2, p);
    return dblcmp(multi(p, l0.a, l0.b)) > 0;
}

void addLine(double x1, double y1, double x2, double y2) {
    l[ln].a.x = x1; l[ln].a.y = y1;
    l[ln].b.x = x2; l[ln].b.y = y2;
    l[ln].angle = atan2(y2-y1, x2-x1);
    order[ln] = ln;
    ln++;
}

void halfPlaneIntersection() {
    int i, j;
    sort(order, order+ln, cmp);
    for (i = 1, j = 0; i < ln; i++)
        if (dblcmp(l[order[i]].angle-l[order[j]].angle) > 0)
            order[++j] = order[i];
    ln = j + 1;
    dq[0] = order[0];
    dq[1] = order[1];
    bot = 0;
    top = 1;
    for (i = 2; i < ln; i++) {
        while (bot < top && judge(l[order[i]], l[dq[top-1]], l[dq[top]])) top--;
        while (bot < top && judge(l[order[i]], l[dq[bot+1]], l[dq[bot]])) bot++;
        dq[++top] = order[i];
    }
    while (bot < top && judge(l[dq[bot]], l[dq[top-1]], l[dq[top]])) top--;
    while (bot < top && judge(l[dq[top]], l[dq[bot+1]], l[dq[bot]])) bot++;
}

bool isThereACore() {
    if (top-bot > 1) return true;
    return false;
}

int main()
{
    int t, i;

    scanf ("%d", &t);
    while (t--) {
        scanf ("%d", &pn);
        for (i = 0; i < pn; i++)
            scanf ("%lf%lf", &p[i].x, &p[i].y);
        for (ln = i = 0; i < pn-1; i++)
            addLine(p[i].x, p[i].y, p[i+1].x, p[i+1].y);
        addLine(p[i].x, p[i].y, p[0].x, p[0].y);
        halfPlaneIntersection();
        if (isThereACore()) printf ("YES\n");
        else printf ("NO\n");
    }
    return 0;
}

 

   基於上文對構造凸包的探討,下面我們來討論如何判斷一個多邊形是否是凸包。(Problem source : pku 1584)
 

Description

The DIY Furniture company specializes in assemble-it-yourself furniture kits. Typically, the pieces of wood are attached to one another using a wooden peg that fits into pre-cut holes in each piece to be attached. The pegs have a circular cross-section and so are intended to fit inside a round hole. A recent factory run of computer desks were flawed when an automatic grinding machine was mis-programmed. The result is an irregularly shaped hole in one piece that, instead of the expected circular shape, is actually an irregular polygon. You need to figure out whether the desks need to be scrapped or if they can be salvaged by filling a part of the hole with a mixture of wood shavings and glue. There are two concerns. First, if the hole contains any protrusions (i.e., if there exist any two interior points in the hole that, if connected by a line segment, that segment would cross one or more edges of the hole), then the filled-in-hole would not be structurally sound enough to support the peg under normal stress as the furniture is used. Second, assuming the hole is appropriately shaped, it must be big enough to allow insertion of the peg. Since the hole in this piece of wood must match up with a corresponding hole in other pieces, the precise location where the peg must fit is known. Write a program to accept descriptions of pegs and polygonal holes and determine if the hole is ill-formed and, if not, whether the peg will fit at the desired location. Each hole is described as a polygon with vertices (x1, y1), (x2, y2), . . . , (xn, yn). The edges of the polygon are (xi, yi) to (x i+1, y i+1) for i = 1 . . . n − 1 and (xn, yn) to (x1, y1).

Input

Input consists of a series of piece descriptions. Each piece description consists of the following data: Line 1 < nVertices > < pegRadius > < pegX > < pegY > number of vertices in polygon, n (integer) radius of peg (real) X and Y position of peg (real) n Lines < vertexX > < vertexY > On a line for each vertex, listed in order, the X and Y position of vertex The end of input is indicated by a number of polygon vertices less than 3.

Output

For each piece description, print a single line containing the string: HOLE IS ILL-FORMED if the hole contains protrusions PEG WILL FIT if the hole contains no protrusions and the peg fits in the hole at the indicated position PEG WILL NOT FIT if the hole contains no protrusions but the peg will not fit in the hole at the indicated position


  題目大意:給出一個圓的半徑和圓心,並順次(順時針或者逆時針都可)給出n個點。首先判斷該n邊形是否是凸包,如果是,判斷該圓是否被抱在這個凸包當中。
  數理分析:通過問題描述,我們不難看出我們需要解決如下兩個問題。
  ①判斷一個多邊形是否是凸包。
  ②判斷圓是否包含在其內部。
  針對第一個問題,我們依舊利用在計算幾何中強有力的工具——叉積。我們遍歷凸包相鄰三點,叉積的值均為非正或非負即表明是一個凸包。這里值得一提的是叉積等於0這種情況,它表示三點共線,如果求解的凸包要求頂點不能共線,只需把上面的結論改成均為正或負即可。
  針對第二個問題,我們再分解其判斷的流程。
  步驟一:要判斷這個圓是否在凸包的內部,首先得判斷該點是否在凸包的內部,同樣可以利用上一段所描述的叉積來實現。
  步驟二:然后我們想到,圓在凸包內部的臨界情況是內切,因此我們可以通過圓心到各邊長的距離和半徑的比較來判斷這個圓是否在凸包的內部。
  這里我們可以看到,如果圓心不在凸包內部,在步驟二的判斷過程中依然會出現符合要求的情況,但是圓卻不在凸包的內部,這佐證了步驟一存在的必要性。
  編程實現:有了上述的數理分析,我們在編程實現的時候需要一些比較巧的編程技巧以簡化代碼。在判斷是否是凸包和判斷圓心是否在凸包內部的時候我么你都用到了遍歷點求叉積的過程,由於這里沒有告訴究竟是順時針給出點集還是逆時針給出,因此我們設置一個標記數組s(初始化為1),叉乘為0、正、負分別記為0、1、2,得到結果后,記s[x] = 0,(x = 0 、1、 2),我們再利用位運算符“|”,當s[1] | s[2] = 0的時候,表明同時出現了叉乘為正和為負的情況,此時即可判斷不符合要求。這就很好解決了點集給出時的方向不確定性的問題。
  參考代碼如下。

 

#include<iostream>
#include<cstdio>
#include<math.h>
#define eps 1e-8
#define _sign(x) (x>eps ? 1 : x < -eps ? 2 : 0)

struct Point{double x , y;};
struct Line{double a , b , c;};
struct Circle{double r;Point center;};

double xmult(Point p1,Point p2,Point p0)
{
    return (p1.x-p0.x)*(p2.y-p0.y) - (p2.x-p0.x)*(p1.y-p0.y);
}

int isConvex(int n,Point *p)
{
     int i , s[3] = {1,1,1};
     for(i = 0;i < n && s[1]|s[2];i++)
            s[_sign(xmult(p[(i+1)%n],p[(i+2)%n],p[i]))] = 0;
       return s[1]|s[2];
}

int insideConvex(Point q,int n,Point *p)
{
    int i , s[3] = {1,1,1};
    for(i = 0;i < n && s[1]|s[2];i++)
           s[_sign(xmult(p[(i+1)%n],q,p[i]))] = 0;
    return s[1]|s[2];
}

double poToLine(Point p , Line l)
{
     return fabs(l.a*p.x + l.b*p.y +l.c)/sqrt(l.a*l.a + l.b*l.b);
}

Line twoPoLine(Point p1, Point p2)
{
     Line l;
     l.a = p1.y - p2.y;
     l.b = p2.x - p1.x;
     l.c = p1.x*p2.y - p2.x*p1.y;
     return l;
}

Point p[10000];

int main()
{
    int n;
    Circle c;
    while(scanf("%d",&n) != EOF,n > 2)
    {
          int i;
             scanf("%lf%lf%lf",&c.r,&c.center.x,&c.center.y);
                for(i = 0;i < n;i++)
                      scanf("%lf%lf",&p[i].x,&p[i].y);
             if(isConvex(n,p))
             {
                    if(insideConvex(c.center,n,p))
                    {
                          for(i = 0;i < n;i++)
                          {
                                Line l = twoPoLine(p[i],p[(i+1)%n]);
                                if(poToLine(c.center,l)-c.r< 0.0)
                                    {printf("PEG WILL NOT FIT\n");break;}

                          }
                             if(i == n)  {printf("PEG WILL FIT\n");}
                    }
                    else
                             printf("PEG WILL NOT FIT\n");
             }
             else
                             printf("HOLE IS ILL-FORMED\n");

    }
    return 0;
}

 

  我們再來看一個有關凸包的問題。(Problem source : pku 2007)

  

Description

A closed polygon is a figure bounded by a finite number of line segments. The intersections of the bounding line segments are called the vertices of the polygon. When one starts at any vertex of a closed polygon and traverses each bounding line segment exactly once, one comes back to the starting vertex.
A closed polygon is called convex if the line segment joining any two points of the polygon lies in the polygon. Figure 1 shows a closed polygon which is convex and one which is not convex. (Informally, a closed polygon is convex if its border doesn't have any "dents".)
The subject of this problem is a closed convex polygon in the coordinate plane, one of whose vertices is the origin (x = 0, y = 0). Figure 2 shows an example. Such a polygon will have two properties significant for this problem.
The first property is that the vertices of the polygon will be confined to three or fewer of the four quadrants of the coordinate plane. In the example shown in Figure 2, none of the vertices are in the second quadrant (where x < 0, y > 0).
To describe the second property, suppose you "take a trip" around the polygon: start at (0, 0), visit all other vertices exactly once, and arrive at (0, 0). As you visit each vertex (other than (0, 0)), draw the diagonal that connects the current vertex with (0, 0), and calculate the slope of this diagonal. Then, within each quadrant, the slopes of these diagonals will form a decreasing or increasing sequence of numbers, i.e., they will be sorted. Figure 3 illustrates this point.

Input

The input lists the vertices of a closed convex polygon in the plane. The number of lines in the input will be at least three but no more than 50. Each line contains the x and y coordinates of one vertex. Each x and y coordinate is an integer in the range -999..999. The vertex on the first line of the input file will be the origin, i.e., x = 0 and y = 0. Otherwise, the vertices may be in a scrambled order. Except for the origin, no vertex will be on the x-axis or the y-axis. No three vertices are colinear.

Output

The output lists the vertices of the given polygon, one vertex per line. Each vertex from the input appears exactly once in the output. The origin (0,0) is the vertex on the first line of the output. The order of vertices in the output will determine a trip taken along the polygon's border, in the counterclockwise direction. The output format for each vertex is (x,y) as shown below.

  題目大意:無序給出凸包的頂點集,其中一個頂點有(0,0),現在要求你按照逆時針輸出該凸包的頂點,從(0,0)開始。

  數理分析:在前文討論構造凸包的高效算法Graham-Scan算法的時候,我們蜻蜓點水般地點了一下極角排序這個概念,這里通過這個問題更加詳細的介紹一下這個概念的含義。   極角排序是基於極坐標來說的。對於極坐標:選取平面內任意一點o做坐標原點,那么平面內的任意點A都可以用(x,y)來表示,其中x = ρcosθ,y = ρsinθ。這里ρ是A到o的距離,θ則是oA與過o的水平線的正方向的夾角。某兩點的θ相同,那么我們選取ρ較小的排在前面。   那么我們根據點集V中各元素極角θ的大小進行排序,然后再次訪問該點集,會發現我們訪問的點總體上是呈逆時針順序的分布了。

  那么我們再回這個問題,由於這里說明了給出的一定是能夠組成凸包的點集,且從(0,0)開始逆時針輸出,那么我們就很自然與上面極角排序的模型進行關聯,其實本質上就是以笛卡爾坐標系下的原點為極坐標系下的原點,將給出的點集進行極角排序,然后依次輸出,便一定是按照逆時針方向輸出凸包頂點。   知道了要用極角排序這一點了,接下來的問題就是如何實現極角排序了。方法有很多,而根據極角排序的定義,在這里給出一種利用計算幾何中最常用的工具——叉積,剛好能夠將其實現。   編程實現:我們通過設置一個bool函數cmp,來給出o、v1、v2的相對位置,然后借用c++中方面的sort排序即可快速完成對整個點集的極角排序。

  參考代碼如下(值得注意的是從題目描述來看,這道題目從屏幕輸出來看是不受參數限制的,因此在編碼的時候用EOF控制結束輸入即可):

 

#include<iostream>
#include<cmath>
#include<stdio.h>
#include<algorithm>
#define EPS 1e-8
using namespace std;
struct point{double x  ,y;};
point convex[50];

double cross(point p1, point p2, point q1,point q2)
{
    return (q2.y - q1.y)*(p2.x - p1.x) - (q2.x - q1.x)*(p2.y - p1.y);
}
bool cmp(point a , point b)
{
     point o;
     o.x = o.y = 0;
     return cross(o,b,o,a) < 0;
}

int main()
{
     int cnt = 0;
     while(scanf("%lf%lf",&convex[cnt].x,&convex[cnt].y) != EOF)
     {
          cnt++;
     }
     sort(convex + 1 , convex + cnt , cmp);
        for(int i = 0;i < cnt;i++)
              cout<<"("<<convex[i].x<<","<<convex[i].y<<")"<<endl;
     return 0;
}

 

 

  通過上文對凸包問題的初步了解,這里我們將介紹旋轉卡殼的相關概念。
  其概念非常簡單、很好理解。


  首先介紹支撐線,給定一個凸多邊形,如果直線L過凸多邊形的頂點,並且該凸多邊形的其余頂點都分布在該直線L的一側,則我們稱這條直線是這條凸多邊形的一條支撐線。
  在支撐線的基礎上,如果凸多邊形存在兩條平行的支撐線,那么這兩條支撐線分別經過的兩個點叫做一對對踵點。
  那么,一個凸多邊形的所有成對的對踵點當中,存在一組兩點相距距離最大的,那么這個長度我們成為凸包的直徑。
  而這些和旋轉卡殼這個名字有什么關系呢?通過其字面意思,聯系我們上文中給出的凸包直徑的概念,如果我們找到表征凸包直徑的對踵點,並畫出兩個點所在的兩條支撐線,此時該凸包其實是可以繞着直徑,在兩條支撐線當中任意旋轉的,這其實就是“旋轉卡殼”想表達的意思,明白算法名稱的含義,將更加得有利於我們理解算法在干什么。
  我們再抽象化的概括一下問題,即給定一個凸包,求其直徑。或者也可以表述成,給定點集,求該點集的任意兩元素的最大距離。
  知道了算法的內涵了,我們就要思考算法是怎么具體實現的了。
  從問題描述上來看,似乎可以用窮舉來實現,但是隨着點集元素的增大,這種暴力窮舉的方法的時間復雜度也會激增。


  有沒有更優化的求解方法呢?
  我們逆向的去思考這個問題,假設我們已經給出了凸包的一對對踵點,但兩點的連線不一定是凸包的直徑,我們根據這兩點也是可以做出兩條互相平行的支撐線,隨后我們讓這兩條互相平行沿着凸包的邊旋轉,顯然,這樣下去會遍歷出所有成對的對踵點所在支撐線平行的情況,而又由於其實狀態下兩條互相平行線之間的距離是確定的,因此當支撐線越靠近凸包的邊,此時夾在兩條平行線之間的對踵點連線也是越“歪斜”的(可以通過作圖實驗或者思維想象一下),也就越長,那么我們顯然能夠看到它的終態:平行線中的一條與凸包的一條邊v(i)v(i+1)重合,那么凸包的直徑就是max(v(q)v(i) , v(q)v(j))。(v(q)表示另一條支撐線經過的那個頂點)。
  這樣在按照某個方向旋轉互相平行的支撐線的時候,一個點的對踵點也是按照這個方向旋轉的,這在維護最大值的時候,相對於窮舉算法,便避免了大量的重復計算。
  我們通過一個題目來具體的實現一下這個算法。(Problem source : pku 2187)

Total Submissions: 32797   Accepted: 10177

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.


  題目大意:給定一個點集,讓你其中兩點之間最大的距離。
  數理分析:基於上面的分析,這里就可以恰到好處的用到凸包問題的旋轉卡殼算法了。
  編程實現:由於時間原因,筆者在這里暫且只貼出代碼而沒有給出注釋和編程實現的分析,在以后修正文章的時候會詳細給出分析的。
 

#include <cmath>
#include<stdio.h>
#include <algorithm>
#include <iostream>
using namespace std;
#define MAXN 50005

struct Point
{
    int x, y;
    bool operator < (const Point& _P) const
    {
        return y<_P.y||(y==_P.y&&x<_P.x);
    };
}pset[MAXN],ch[MAXN];

double cross(Point a,Point b,Point o)
{
    return (a.x - o.x) * (b.y - o.y) - (b.x - o.x) * (a.y - o.y);
}
void convex_hull(Point *p,Point *ch,int n,int &len)
{
    sort(p, p+n);
    ch[0]=p[0];
    ch[1]=p[1];
    int top=1;
    for(int i=2;i<n;i++)
    {
        while(top>0&&cross(ch[top],p[i],ch[top-1])<=0)
            top--;
        ch[++top]=p[i];
    }
    int tmp=top;
    for(int i=n-2;i>=0;i--)
    {
        while(top>tmp&&cross(ch[top],p[i],ch[top-1])<=0)
            top--;
        ch[++top]=p[i];
    }
    len=top;
}



int dist2(Point a,Point b)
{
    return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
}

int rotating_calipers(Point *ch,int n)
{
    int q=1,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(dist2(ch[p],ch[q]),dist2(ch[p+1],ch[q+1])));
    }
    return ans;
}

int main()
{
    
    int n, len;
    while(scanf("%d", &n)!=EOF)
    {
        for(int i = 0;i < n;i++)
        {
            scanf("%d %d",&pset[i].x,&pset[i].y);
        }
        convex_hull(pset,ch,n,len);
        printf("%d\n",rotating_calipers(ch,len));
    }
    return 0;
}

 


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM