題目:http://lightoj.com/volume_showproblem.php?problem=1190
參考鏈接:https://blog.csdn.net/gkingzheng/article/details/81836308
http://www.mamicode.com/info-detail-1555428.html
一、比如說,我就隨便塗了一個多邊形和一個點,現在我要給出一種通用的方法來判斷這個點是不是在多邊形內部(別告訴我用肉眼觀察……)。

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

這個結論很簡單,那它是怎么來的?下面就簡單講解一下。
首先,對於平面內任意閉合曲線,我們都可以直觀地認為,曲線把平面分割成了內、外兩部分,其中“內”就是我們所謂的多邊形區域。

基於這一認識,對於平面內任意一條直線,我們可以得出下面這些結論:
- 直線穿越多邊形邊界時,有且只有兩種情況:進入多邊形或穿出多邊形。
- 在不考慮非歐空間的情況下,直線不可能從內部再次進入多邊形,或從外部再次穿出多邊形,即連續兩次穿越邊界的情況必然成對。
- 直線可以無限延伸,而閉合曲線包圍的區域是有限的,因此最后一次穿越多邊形邊界,一定是穿出多邊形,到達外部。

現在回到我們最初的題目。假如我們從一個給定的點做射線,還可以得出下面兩條結論:
- 如果點在多邊形內部,射線第一次穿越邊界一定是穿出多邊形。
- 如果點在多邊形外部,射線第一次穿越邊界一定是進入多邊形。

把上面這些結論綜合起來,我們可以歸納出:
- 當射線穿越多邊形邊界的次數為偶數時,所有第偶數次(包括最后一次)穿越都是穿出,因此所有第奇數次(包括第一次)穿越為穿入,由此可推斷點在多邊形外部。

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

到這里,我們已經了解了這個解法的思路,大家可以試着自己寫一個實現出來。關於算法實現中某些具體問題和邊界條件的處理,下次接着寫,這次畫圖已經畫夠了……
后記:給出這個解法后,我簡單搜了一下,原來這種算法就叫做射線法(ray casting)或者奇偶規則法(even odd rule),是一種早已被廣泛應用的算法。后面還打算介紹另一種通過回轉數(winding number,拓撲學的一個概念)解這個問題的思路。
二、射線法在實際應用中的一些問題和解決方案。
-
點在多邊形的邊上
前面我們講到,射線法的主要思路就是計算射線穿越多邊形邊界的次數。那么對於點在多邊形的邊上這種特殊情況,射線出發的這一次,是否應該算作穿越呢?

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

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

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

解決方案:
-
判斷點是否在線上的方法有很多,比較簡單直接的就是計算點與兩個多邊形頂點的連線斜率是否相等,中學數學都學過。
-
點和多邊形頂點重合的情況更簡單,直接比較點的坐標就行了。
-
頂點穿越看似棘手,其實我們換一個角度,思路會大不相同。先來回答一個問題,射線穿越一條線段需要什么前提條件?沒錯,就是線段兩個端點分別在射線兩側。只要想通這一點,頂點穿越就迎刃而解了。這樣一來,我們只需要規定被射線穿越的點都算作其中一側。

如上圖,假如我們規定射線經過的點都屬於射線以上的一側,顯然點D和發生頂點穿越的點C都位於射線Y的同一側,所以射線Y其實並沒有穿越CD這條邊。而點C和點B則分別位於射線Y的兩側,所以射線Y和BC發生了穿越,由此我們可以斷定點Y在多邊形內。同理,射線X分別與AD和CD都發生了穿越,因此點X在多邊形外,而射線Z沒有和多邊形發生穿越,點Z位於多邊形外。
-
解決了第三點,這一點就毫無難度了。根據上面的假設,射線連續經過的兩個頂點顯然都位於射線以上的一側,因此這種情況看作沒有發生穿越就可以了。由於第三點的解決方案實際上已經覆蓋到這種特例,因此不需要再做特別的處理
- 代碼:
/*
射線法:判斷一個點是在多邊形內部,邊上還是在外部,時間復雜度為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;
}
