射線法(1190 - Sleepwalking )


題目:http://lightoj.com/volume_showproblem.php?problem=1190

 參考鏈接:https://blog.csdn.net/gkingzheng/article/details/81836308

http://www.mamicode.com/info-detail-1555428.html

一、比如說,我就隨便塗了一個多邊形和一個點,現在我要給出一種通用的方法來判斷這個點是不是在多邊形內部(別告訴我用肉眼觀察……)。

enter image description here

首先想到的一個解法是從這個點做一條射線,計算它跟多邊形邊界的交點個數,如果交點個數為奇數,那么點在多邊形內部,否則點在多邊形外。

enter image description here

這個結論很簡單,那它是怎么來的?下面就簡單講解一下。

首先,對於平面內任意閉合曲線,我們都可以直觀地認為,曲線把平面分割成了內、外兩部分,其中“內”就是我們所謂的多邊形區域。

enter image description here

基於這一認識,對於平面內任意一條直線,我們可以得出下面這些結論:

  • 直線穿越多邊形邊界時,有且只有兩種情況:進入多邊形或穿出多邊形。
  • 在不考慮非歐空間的情況下,直線不可能從內部再次進入多邊形,或從外部再次穿出多邊形,即連續兩次穿越邊界的情況必然成對。
  • 直線可以無限延伸,而閉合曲線包圍的區域是有限的,因此最后一次穿越多邊形邊界,一定是穿出多邊形,到達外部。

enter image description here

現在回到我們最初的題目。假如我們從一個給定的點做射線,還可以得出下面兩條結論:

  • 如果點在多邊形內部,射線第一次穿越邊界一定是穿出多邊形。
  • 如果點在多邊形外部,射線第一次穿越邊界一定是進入多邊形。

enter image description here

把上面這些結論綜合起來,我們可以歸納出:

  • 當射線穿越多邊形邊界的次數為偶數時,所有第偶數次(包括最后一次)穿越都是穿出,因此所有第奇數次(包括第一次)穿越為穿入,由此可推斷點在多邊形外部。enter image description here
  • 當射線穿越多邊形邊界的次數為奇數時,所有第奇數次(包括第一次和最后一次)穿越都是穿出,由此可推斷點在多邊形內部。enter image description here

到這里,我們已經了解了這個解法的思路,大家可以試着自己寫一個實現出來。關於算法實現中某些具體問題和邊界條件的處理,下次接着寫,這次畫圖已經畫夠了……

后記:給出這個解法后,我簡單搜了一下,原來這種算法就叫做射線法(ray casting)或者奇偶規則法(even odd rule),是一種早已被廣泛應用的算法。后面還打算介紹另一種通過回轉數(winding number,拓撲學的一個概念)解這個問題的思路。

 

二、射線法在實際應用中的一些問題和解決方案。

  1. 點在多邊形的邊上

    前面我們講到,射線法的主要思路就是計算射線穿越多邊形邊界的次數。那么對於點在多邊形的邊上這種特殊情況,射線出發的這一次,是否應該算作穿越呢?

    enter image description here

    看了上面的圖就會發現,不管算不算穿越,都會陷入兩難的境地——同樣落在多邊形邊上的點,可能會得到相反的結果。這顯然是不正確的,因此對這種特殊情況需要特殊處理。

  2. 點和多邊形的頂點重合

    enter image description here

    這其實是第一種情況的一個特例。

  3. 射線經過多邊形頂點

    射線剛好經過多邊形頂點的時候,應該算一次還是兩次穿越?這種情況比前兩種復雜,也是實現中的難點,后面會講解它的解決方案。

    enter image description here

  4. 射線剛好經過多邊形的一條邊

    這是上一種情況的特例,也就是說,射線連續經過了多邊形的兩個相鄰頂點。

    enter image description here

解決方案:

  1. 判斷點是否在線上的方法有很多,比較簡單直接的就是計算點與兩個多邊形頂點的連線斜率是否相等,中學數學都學過。

  2. 點和多邊形頂點重合的情況更簡單,直接比較點的坐標就行了。

  3. 頂點穿越看似棘手,其實我們換一個角度,思路會大不相同。先來回答一個問題,射線穿越一條線段需要什么前提條件?沒錯,就是線段兩個端點分別在射線兩側。只要想通這一點,頂點穿越就迎刃而解了。這樣一來,我們只需要規定被射線穿越的點都算作其中一側。

    enter image description here

    如上圖,假如我們規定射線經過的點都屬於射線以上的一側,顯然點D和發生頂點穿越的點C都位於射線Y的同一側,所以射線Y其實並沒有穿越CD這條邊。而點C和點B則分別位於射線Y的兩側,所以射線Y和BC發生了穿越,由此我們可以斷定點Y在多邊形內。同理,射線X分別與AD和CD都發生了穿越,因此點X在多邊形外,而射線Z沒有和多邊形發生穿越,點Z位於多邊形外。

  4. 解決了第三點,這一點就毫無難度了。根據上面的假設,射線連續經過的兩個頂點顯然都位於射線以上的一側,因此這種情況看作沒有發生穿越就可以了。由於第三點的解決方案實際上已經覆蓋到這種特例,因此不需要再做特別的處理

  5. 代碼:
    /*
    射線法:判斷一個點是在多邊形內部,邊上還是在外部,時間復雜度為O(n);
    射線法可以正確用於凹多邊形;
    射線法是使用最廣泛的算法,這是由於相比較其他算法而言,它不但可以正
    確使用在凹多邊形上,而且不需要考慮精度誤差問題。該算法思想是從點出
    發向右水平做一條射線,計算該射線與多邊形的邊的相交點個數,當點不在
    多邊形邊上時,如果是奇數,那么點就一定在多邊形內部,否則,在外部。
    */
    #include <stdio.h>
    #include <algorithm>
    #include <cstring>
    #include <cmath>
    using namespace std;
    const int N = 2010;
    const double eps = 1e-10;
    const int INF = 0x3f3f3f3f;
    //////////////////////////////////////////////////////////////////
    struct point
    {
        double x, y;
        point(double x=0, double y=0) : x(x), y(y){}
        friend point operator - (const point& p1, const point& p2)
        {
            return point(p1.x-p2.x, p1.y-p2.y);
        }
        friend double operator ^ (const point& p1, const point& p2)
        {
            return p1.x*p2.y - p1.y*p2.x;
        }
    };
    //////////////////////////////////////////////////////////////////
    struct Segment
    {
        point s, e;
    };
    //////////////////////////////////////////////////////////////////
    ///判斷一個double類型的數是  0  <0  >0;
    int Sign(double x)
    {
        if( fabs(x) < eps )return 0;
        if(x > 0)return 1;
        return -1;
    }
    //////////////////////////////////////////////////////////////////
    ///判斷o在ab的哪邊;0:o在直線ab上; >0:在左邊; <0:在右邊;
    double cross(point o, point a, point b)
    {
        return ((a-o)^(b-o));
    }
    //////////////////////////////////////////////////////////////////
    ///已知abc三點在一條直線上,判斷點a是否在線段bc之間;<=0:在   >0:不在;
    int Between(point a, point b, point c)
    {
        if(fabs(b.x-c.x) > fabs(b.y-c.y))
            return Sign(min(b.x, c.x)-a.x)*Sign(max(b.x, c.x)-a.x);
        else
            return Sign(min(b.y, c.y)-a.y)*Sign(max(b.y, c.y)-a.y);
    }
    //////////////////////////////////////////////////////////////////
    ///判斷點p0和線段S上,<=0:在,1:不在;
    int PointOnSegment(point p0, Segment S)
    {
        if(Sign(cross(S.s, S.e, p0)) == 0)
            return Between(p0, S.s, S.e);
        return 1;
    }
    //////////////////////////////////////////////////////////////////
    ///求線段a和線段b的交點個數;
    int SegmentCross(Segment a, Segment b)
    {
        double x1 = cross(a.s, a.e, b.s);
        double x2 = cross(a.s, a.e, b.e);
        double x3 = cross(b.s, b.e, a.s);
        double x4 = cross(b.s, b.e, a.e);
        if(Sign(x1*x2)<0 && Sign(x3*x4)<0) return 1;
        if((Sign(x1)==0 && Between(b.s, a.s, a.e)<=0) ||
           (Sign(x2)==0 && Between(b.e, a.s, a.e)<=0) ||
           (Sign(x3)==0 && Between(a.s, b.s, b.e)<=0) ||
           (Sign(x4)==0 && Between(a.e, b.s, b.e)<=0))
           return 2;
        return 0;
    }
    //////////////////////////////////////////////////////////////////
    ///判斷點p0與含有n個節點的多邊形的位置關系,p數組是頂點集合;
    ///返回0:邊上或頂點上,    1:外面,   -1:里面;
    int PointInPolygon(point p0, point p[], int n)
    {
        Segment L, S;
        point temp;
        L.s = p0, L.e = point(INF, p0.y);///以p0為起點的射線L;
        int counts = 0;
        p[n] = p[0];
        for(int i=1; i<=n; i++)
        {
            S.s = p[i-1], S.e = p[i];
            if(PointOnSegment(p0, S) <= 0) return 0;
            if(S.s.y == S.e.y) continue;///和射線平行;
            if(S.s.y > S.e.y) temp = S.s;
            else temp = S.e;
            if(PointOnSegment(temp, L) == -1)
                counts ++;
            else if(SegmentCross(L, S) == 1)
                counts ++;
        }
        if(counts%2) return -1;
        return 1;
    }
    //////////////////////////////////////////////////////////////////
    int main()
    {
        point p[N];
        int T, tCase = 1, n, q;
        scanf("%d", &T);
        while(T--)
        {
            scanf("%d", &n);
            for(int i=0; i<n; i++)
                scanf("%lf %lf", &p[i].x, &p[i].y);
            scanf("%d", &q);
            printf("Case %d:\n", tCase++);
            for(int i=1; i<=q; i++)
            {
                int x, y;
                scanf("%d %d", &x, &y);
                int ans = PointInPolygon(point(x, y), p, n);
                if(ans == 1) puts("No");
                else puts("Yes");
            }
        }
        return 0;
    }


免責聲明!

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



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