【詳解】半平面交算法入門詳解(計算幾何)

半平面交

簡介

博客背景

筆者在學習半平面交時,網上找入門博客資源甚少,且大部分難以理解,故在稍稍入門了半平面交後,寫此博客,希望能對大家有所幫助。若有錯誤,麻煩指出。

半平面交是什麼?

我們知道一條直線可以把平面分爲兩部分,其中一半的平面就叫半平面。
那半平面交,就是多個半平面的相交部分。我們在學習線性規劃時就有用過。

半平面交有什麼用?

1.求解一個區域,可以看到給定圖形的各個角落。(多邊形的核)
2.求可以放進多邊形的圓的最大半徑。

求解半平面交的步驟(S&I算法 O(nlogn))

我們試着來解決 “求解一個區域,可以看到給定圖形的各個角落。”
爲了敘述方便,我們把這個區域叫做多邊形的核。

1.選取一個正方向。(一般爲逆時針)

我們用這個一個不規則圖形舉例子。

首先我們選逆時針方向做爲有向線段。

這樣選取的好處是,保證核在有向線段的左邊。

2.把有向線段通過極角排序(與 xx 軸的夾角)(-180°,180°]

排序結果如下所示。

按照極角排序的原因是寫代碼方便,排序之後的線段是有序的,可以在雙端隊列裏進行操作。(下面會再解釋)。

3.按順序遍歷每條線段,取左邊區域,刪右邊區域

我們用這個 S&I 算法求解半平面交時,用的是刪減法,首先我們假設全部平面都是半平面交,然後不斷加入直線,不斷刪去右邊區域,保留左邊區域。最後剩下的區域就是需要求的半平面交。

1.全部平面都是半平面交。
2.加入第一條直線,保留左邊區域,刪除右邊區域。
3.加入第二條線段,保留左邊區域,刪除右邊區域。
4.依次加入3 - 10線段,保留左邊區域,刪除右邊區域。
5.加入最後一條線段,保留左邊區域,刪除右邊區域。
6.剩下的藍色部分,就是多邊形的和,也就是所有直線的半平面交,在藍色區域的任何一點,都可以看到多邊形的每一個角落。
7.這時我們得到的是圍成這個藍色區域的直線集合。

L={257911}L = \{2,5,7,9,11\} ,如果至少有三條邊,就說明該多邊形有核(三條以上時,核爲全部直線圍成的凸包。)如果要求面積,我們可以將直線的交點求出來,然後再用叉積求凸包面積。

4.如果題目要求求面積。

我們可以發現求出來的直線的集合是有序的 L={257911}L = \{2,5,7,9,11\},這些直線剛好是逆時針圍着這個半平面交。(這就是按極角排序的好處)。如果要求面積,我們可以把所有L[i]L[i]L[i+1]L[i + 1] 的交點求出來,然後用叉乘求凸包面積。

5.總結

總體而言,求半平面交其實就是維護線段的集合 LL,遍歷每一條線段,判斷這條線段加入後對於半平面交的影響,然後在集合 LL 中剔除掉對半平面交沒有決定作用的邊,留下起決定作用的邊。即最終目的是維護半平面交的線段集合 LL

6.算法優化

1.同極角時,排序後可以去掉右邊的線段,保留左邊的線段。

例如上述步驟 3-3 時,加入第二條線段。不難發現,當①號線段和②號線段的極角相同時,①號線段沒有意義。因爲①號線段在②號線段右邊。因此在排序後,可以去掉沒有意義的線段,即保留極角相同的情況下最左邊的線段。

算法實現 S&IS\&I算法 OO(nlognnlogn)

算法流程

1.以逆時針爲正方向,建邊。(輸入方向不確定時,可用叉乘求面積看正負得知輸入的順逆方向。)
2.對線段根據極角排序。
3.去除極角相同的情況下,位置在右邊的邊。
4.用雙端隊列儲存線段集合 LL,遍歷所有線段。
5.判斷該線段加入後對半平面交的影響,(對雙端隊列的頭部和尾部進行判斷,因爲線段加入是有序的。)。
6.如果某條線段對於新的半平面交沒有影響,則從隊列中剔除掉。
7.最後剩下的線段集合 LL,即使最後要求的半平面交。

疑問解答

1.爲什麼要用雙端隊列?

因爲線段是按照極角排序的,所以可以形成環,如圖,原來的線段集合爲
L={1234567}L = \{1,2,3,4,5,6,7\}。現在我們想把線段 8 加入到線段集中,顯然核的形成和線段1、6、7已經沒有關係了,因此我們應該在隊列的頭部找到線段 1,把它刪去,然後在隊列的尾部找到線段6、7,然後刪除掉。

2.線段這麼纔對半平面交沒有影響?

在下圖中,藍色爲當前半平面交。

當我們加入紅色線段時,半平面交產生了變化。

因爲我們對線段進行了排序,所以加入的線段會比前面的更“陡”。顯然,如果先前的兩條線段的交點在當前加入線段的右側,則較“陡”的那條線段就會無效。

代碼實現

poj3335

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
const int maxn = 1e3;
const double EPS = 1e-5;
int T, n;
typedef struct Grid {
  double x, y;
  Grid(double a = 0, double b = 0) {x = a, y = b;}
} Point, Vector;
Vector operator - (Point a, Point b) {return Vector(b.x - a.x, b.y - a.y);}
double operator ^ (Vector a, Vector b) {return a.x * b.y - a.y * b.x;}//叉乘
struct Line {
  Point s, e;
  Line() {}
  Line(Point a, Point b) {s = a, e = b;}
};
Point p[maxn];
Line L[maxn], que[maxn];

//得到極角角度
double getAngle(Vector a) {
  return atan2(a.y, a.x);
}

//得到極角角度
double getAngle(Line a) {
  return atan2(a.e.y - a.s.y, a.e.x - a.s.x);
}

//排序:極角小的排前面,極角相同時,最左邊的排在最後面,以便去重
bool cmp(Line a, Line b) {
  Vector va = a.e - a.s, vb = b.e - b.s;
  double A =  getAngle(va), B = getAngle(vb);
  if (fabs(A - B) < EPS) return ((va) ^ (b.e - a.s)) >= 0;
  return A < B;
}

//得到兩直線相交的交點
Point getIntersectPoint(Line a, Line b) {
  double a1 = a.s.y - a.e.y, b1 = a.e.x - a.s.x, c1 = a.s.x * a.e.y - a.e.x * a.s.y;
  double a2 = b.s.y - b.e.y, b2 = b.e.x - b.s.x, c2 = b.s.x * b.e.y - b.e.x * b.s.y;
  return Point((c1*b2-c2*b1)/(a2*b1-a1*b2), (a2*c1-a1*c2)/(a1*b2-a2*b1));
}

//判斷 b,c 的交點是否在 a 的右邊
bool onRight(Line a, Line b, Line c) {
  Point o = getIntersectPoint(b, c);
  if (((a.e - a.s) ^ (o - a.s)) < 0) return true;
  return false;
}

bool HalfPlaneIntersection() {
  sort(L, L + n, cmp);//排序
  int head = 0, tail = 0, cnt = 0;//模擬雙端隊列
  //去重,極角相同時取最後一個。
  for (int i = 0; i < n - 1; i++) {
    if (fabs(getAngle(L[i]) - getAngle(L[i + 1])) < EPS) {
      continue;
    }
    L[cnt++] = L[i];
  }
  L[cnt++] = L[n - 1];


  for (int i = 0; i < cnt; i++) {
    //判斷新加入直線產生的影響
    while(tail - head > 1 && onRight(L[i], que[tail - 1], que[tail - 2])) tail--;
    while(tail - head > 1 && onRight(L[i], que[head], que[head + 1])) head++;
    que[tail++] = L[i];
  }
  //最後判斷最先加入的直線和最後的直線的影響
  while(tail - head > 1 && onRight(que[head], que[tail - 1], que[tail - 2])) tail--;
  while(tail - head > 1 && onRight(que[tail - 1], que[head], que[head + 1])) head++;
  if (tail - head < 3) return false;
  return true;
}

//判斷輸入點的順序,如果面積 <0,說明輸入的點爲逆時針,否則爲順時針
bool judge() {
  double ans = 0;
  for (int i = 1; i < n - 1; i++) {
    ans += ((p[i] - p[0]) ^ (p[i + 1] - p[0]));
  }
  return ans < 0;
}

int main()
{
  scanf("%d", &T);
  while (T--) {
    scanf("%d", &n);
    for (int i = n - 1; i >= 0; i--) {
      scanf("%lf %lf", &p[i].x, &p[i].y);
    }

    if (judge()) {//判斷輸入順序,保證逆時針連邊。
      for (int i = 0; i < n; i++) {
        L[i] = Line(p[(i + 1)%n], p[i]);
      }
    } else {
      for (int i = 0; i < n; i++) {
        L[i] = Line(p[i], p[(i + 1)%n]);
      }
    }

    if (HalfPlaneIntersection()) printf("YES\n");
    else printf("NO\n");
  }

  return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章