詳談判斷點在多邊形內的七種方法(最全面) hdu1756 hrbust1429 爲例

這幾天在學計算幾何,學到點定位的判斷點在多邊形內,書上提到了三種方法,但是有些方法的代碼不全。於是網上找了找,又發現更多判斷的方法,一時興起決定學習一下,看看到底有多少種,結果一個大坑。。。
網上好多介紹的不詳細(特別是轉角法,最後還是google出來的),而且有些方法叫不同的名字,有點難搞啊,花了我一天多的時間。。TAT

話不多說,下面分享一下。有些方法我會介紹清楚但不會畫圖詳解,希望大家自己畫圖體會。

涉及到的題目
hdu1756
就是裸題,多邊形點順時針給出(有的算法會強調給出順序)

射線法

時間複雜度:O(n) 適用範圍:任意多邊形
個人認爲是非常不錯的算法(不需考慮精度誤差和多邊形點給出的順序),可以作爲第一選擇。

算法思想:
以被測點Q爲端點,向任意方向作射線(一般水平向右作射線),統計該射線與多邊形的交點數。如果爲奇數,Q在多邊形內;如果爲偶數,Q在多邊形外。計數的時候會有一些特殊情況,如圖

圖片已經把特殊情況和算法實現說的很清楚了,下面我直接貼代碼,具體可看代碼註釋。

const double eps = 1e-6;
const double PI = acos(-1);
//三態函數,判斷兩個double在eps精度下的大小關係
int dcmp(double x)
{
    if(fabs(x)<eps) return 0;
    else
        return x<0?-1:1;
}
//判斷點Q是否在P1和P2的線段上
bool OnSegment(Point P1,Point P2,Point Q)
{
    //前一個判斷點Q在P1P2直線上 後一個判斷在P1P2範圍上
    return dcmp((P1-Q)^(P2-Q))==0&&dcmp((P1-Q)*(P2-Q))<=0;
}
//判斷點P在多邊形內-射線法
bool InPolygon(Point P)
{
    bool flag = false; //相當於計數
    Point P1,P2; //多邊形一條邊的兩個頂點
    for(int i=1,j=n;i<=n;j=i++)
    {
        //polygon[]是給出多邊形的頂點
        P1 = polygon[i];
        P2 = polygon[j];
        if(OnSegment(P1,P2,P)) return true; //點在多邊形一條邊上
        //前一個判斷min(P1.y,P2.y)<P.y<=max(P1.y,P2.y)
        //這個判斷代碼我覺得寫的很精妙 我網上看的 應該是大神模版
        //後一個判斷被測點 在 射線與邊交點 的左邊
        if( (dcmp(P1.y-P.y)>0 != dcmp(P2.y-P.y)>0) && dcmp(P.x - (P.y-P1.y)*(P1.x-P2.x)/(P1.y-P2.y)-P1.x)<0)
            flag = !flag;
    }
    return flag;
}

hdu1756-射線法代碼

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const double eps = 1e-6;
const double PI = acos(-1);

int n,m;

struct Point{
    double x,y;
    Point(double x=0,double y=0):x(x),y(y){}

    //向量+
    Point operator +(const Point &b)const
    {
        return Point(x+b.x,y+b.y);
    }
    //向量-
    Point operator -(const Point &b)const
    {
        return Point(x-b.x,y-b.y);
    }
    //點積
    double operator *(const Point &b)const
    {
        return x*b.x + y*b.y;
    }
    //叉積
    //P^Q>0,P在Q的順時針方向;<0,P在Q的逆時針方向;=0,P,Q共線,可能同向或反向
    double operator ^(const Point &b)const
    {
        return x*b.y - b.x*y;
    }
}polygon[105];
typedef Point Vector;

//三態函數,判斷兩個double在eps精度下的大小關係
int dcmp(double x)
{
    if(fabs(x)<eps) return 0;
    else
        return x<0?-1:1;
}

//判斷點Q是否在P1和P2的線段上
bool OnSegment(Point P1,Point P2,Point Q)
{
    return dcmp((P1-Q)^(P2-Q))==0&&dcmp((P1-Q)*(P2-Q))<=0;
}

//判斷點P在多邊形內-射線法
bool InPolygon(Point P)
{
    bool flag = false;
    Point P1,P2; //多邊形一條邊的兩個頂點
    for(int i=1,j=n;i<=n;j=i++)
    {
        P1 = polygon[i];
        P2 = polygon[j];
        if(OnSegment(P1,P2,P)) return true; //點在多邊形一條邊上
        if( (dcmp(P1.y-P.y)>0 != dcmp(P2.y-P.y)>0) && dcmp(P.x - (P.y-P1.y)*(P1.x-P2.x)/(P1.y-P2.y)-P1.x)<0)
            flag = !flag;
    }
    return flag;
}

int main()
{
    while(~scanf("%d",&n))
    {
        for(int i=1;i<=n;i++) scanf("%lf %lf",&polygon[i].x,&polygon[i].y);
        Point test;
        scanf("%d",&m);
        while(m--)
        {
            scanf("%lf %lf",&test.x,&test.y);
            if(InPolygon(test)) printf("Yes\n");
            else printf("No\n");
        }
    }
    return 0;
}

角度和判斷法

時間複雜度:O(n) 適用範圍:任意多邊形
感覺這個方法和之後要介紹的轉角法類似,個人感覺轉角法就是這個方法的優化變形。個人非常不推薦這個算法,最好就是不用。這個算法對精度的要求很高(會造成很大精度誤差),不強調多邊形點給出順序,我用這個算法沒過hdu1756。

算法思想:
連接被測點與多邊形所有頂點所形成的所有角的角度和在精度範圍內等於2π 則該點在多邊形內,否則在多邊形外。
不推薦,所以就不畫圖了。

const double eps = 1e-6;
const double PI = acos(-1);
//三態函數,判斷兩個double在eps精度下的大小關係
int dcmp(double x)
{
    if(fabs(x)<eps) return 0;
    else
        return x<0?-1:1;
}
//判斷點Q是否在P1和P2的線段上
bool OnSegment(Point P1,Point P2,Point Q)
{
    //前一個判斷點Q在P1P2直線上 後一個判斷在P1P2範圍上
    return dcmp((P1-Q)^(P2-Q))==0&&dcmp((P1-Q)*(P2-Q))<=0;
}
//判斷點P在多邊形內-角度和判斷法
//精度要求高 最好不用
bool InPolygon(Point P)
{
    double angle = 0;
    Point P1,P2; //多邊形一條邊的兩個頂點
    Vector V1,V2; //以被測點爲原點 P1 P2與P形成的向量
    for(int i=1,j=n;i<=n;j=i++)
    {
        P1 = polygon[i];
        P2 = polygon[j];
        if(OnSegment(P1,P2,P)) return true; //點在多邊形一條邊上
        V1 = P1-P;
        V2 = P2-P;
        double res = atan2(V2.y,V2.x)-atan2(V1.y,V1.x);
        res = abs(res);
        if(dcmp(res-PI)>0) res = 2*PI-res;
        angle += res;
    }
    return dcmp(2*PI-angle)==0;
}

atan2(y,x)計算y/x的反tan值,在[π,+π ]範圍內。
hdu1756的該方法的代碼沒過,就不貼了。

轉角法

時間複雜度:O(n) 適用範圍:任意多邊形
個人感覺是O(n)算法的第二推薦,該算法本來對精度要求較高,之後會有一個改進讓其不用考慮精度誤差,不過該算法要強調多邊形點給出的順序。
一般博客都以多邊形正向即逆時針介紹,我這裏也主要介紹逆時針,但hdu1756是順時針給出,我會在括號中介紹一下順時針(其實本質是一樣的),順時針具體在代碼註釋中提一下。不會畫圖,希望大家看了思想自己畫圖體會一下。

算法思想:
轉角法非常簡單,按照多邊形頂點逆時針順序,從P點到頂點Vi分別做連線,其中αi爲Vi和Vi+1之間的夾角。其中α角度逆時針爲正,順時針爲負,這樣所有到頂點做連線之間夾角和爲(環繞數)0,這點P在多邊形外部,否則在內部。(感覺和角度和判斷法本質一樣,加了個方向)
(順時針就是角度順時針爲正,逆時針爲負)

直接環繞數的推導會需要用到反三角函數,這樣即會耗時又會造成較大的精度誤差,所以這裏有一個優化。
從P點向右做射線R,如果邊從射線R下方跨到上方,那麼穿越+1,如果從上方跨到下方,則是-1。最終和爲wn環繞數。如下圖所示:
(寫博客的時候才發現其實本質不就是射線嗎,不過理解後代碼會感覺寫的比射線簡單)

這種方法不必去計算射線和邊的交點,但需要判斷點P和邊的左右關係,而且對於方向向上和向下的邊的判斷規則不同。對於方向向上的邊,如果穿過射線,那麼P是在有向邊的左側;方向向下的邊如果穿過射線,那麼P在有向邊的右邊(意思是說判斷點P與邊的關係,而不是相對座標系內位置)。

這裏有一點要注意,如下圖

這裏射線經過了BCCD並穿過C點,但是要計算兩次,一次+1一次-1,代碼中會有體現。

const double eps = 1e-6;
const double PI = acos(-1);
//三態函數,判斷兩個double在eps精度下的大小關係
int dcmp(double x)
{
    if(fabs(x)<eps) return 0;
    else
        return x<0?-1:1;
}
//判斷點Q是否在P1和P2的線段上
bool OnSegment(Point P1,Point P2,Point Q)
{
    //前一個判斷點Q在P1P2直線上 後一個判斷在P1P2範圍上
    return dcmp((P1-Q)^(P2-Q))==0&&dcmp((P1-Q)*(P2-Q))<=0;
}
//判斷點P在多邊形內-轉角法(多邊形點順時針給出)
bool InPolygon(Point P)
{
    int wn = 0;
    Point P1,P2; //多邊形一條邊的兩個頂點
    Vector V1,V2; //被測點與P1分別於P2形成的向量
    for(int i=1,j=n;i<=n;j=i++)
    {
        P1 = polygon[j];  //順時針中前一點
        P2 = polygon[i];  //順時針中後一點
        if(OnSegment(P1,P2,P)) return true; //點在多邊形一條邊上
        V1 = P2-P1;
        V2 = P-P1;
        int k = dcmp(V1^V2); //用於判斷被測點在有向邊的左右
        int d1 = dcmp(P1.y-P.y); //用於判斷向上還是向下穿過
        int d2 = dcmp(P2.y-P.y);
        //V1在V2的順時針方向即測試點在有向邊左邊 並且有向邊向上穿過
        if(k>0 && d1<=0&&d2>0) wn--;  
        //V1在V2的逆時針方向即測試點在有向邊右邊 並且有向邊向下穿過
        if(k<0 && d1>0&&d2<=0) wn++;  
//上面wn+和wn- 一個允許起點在射線上另一個允許終點在射線上 最後特殊情況就會算兩次
//逆時針的wn+和wn-自己畫圖體會一下
    }
    //不爲0即在多邊形內 不管是正還是負
    return wn!=0;
}

hdu1756-轉角法代碼

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const double eps = 1e-6;
const double PI = acos(-1);

int n,m;

struct Point{
    double x,y;
    Point(double x=0,double y=0):x(x),y(y){}

    //向量+
    Point operator +(const Point &b)const
    {
        return Point(x+b.x,y+b.y);
    }
    //向量-
    Point operator -(const Point &b)const
    {
        return Point(x-b.x,y-b.y);
    }
    //點積
    double operator *(const Point &b)const
    {
        return x*b.x + y*b.y;
    }
    //叉積
    //P^Q>0,P在Q的順時針方向;<0,P在Q的逆時針方向;=0,P,Q共線,可能同向或反向
    double operator ^(const Point &b)const
    {
        return x*b.y - b.x*y;
    }
}polygon[105];
typedef Point Vector;

//三態函數,判斷兩個double在eps精度下的大小關係
int dcmp(double x)
{
    if(fabs(x)<eps) return 0;
    else
        return x<0?-1:1;
}

//判斷點Q是否在P1和P2的線段上
bool OnSegment(Point P1,Point P2,Point Q)
{
    return dcmp((P1-Q)^(P2-Q))==0&&dcmp((P1-Q)*(P2-Q))<=0;
}

//判斷點P在多邊形內-轉角法(多邊形點順時針給出)
bool InPolygon(Point P)
{
    int wn = 0;
    Point P1,P2; //多邊形一條邊的兩個頂點
    Vector V1,V2; //被測點與P1分別於P2形成的向量
    for(int i=1,j=n;i<=n;j=i++)
    {
        P1 = polygon[j];  //順時針中前一點
        P2 = polygon[i];  //順時針中後一點
        if(OnSegment(P1,P2,P)) return true; //點在多邊形一條邊上
        V1 = P2-P1;
        V2 = P-P1;
        int k = dcmp(V1^V2);
        int d1 = dcmp(P1.y-P.y);
        int d2 = dcmp(P2.y-P.y);
        if(k>0 && d1<=0&&d2>0) wn--;  //測試點在有向邊左邊 有向邊向上穿過
        if(k<0 && d1>0&&d2<=0) wn++;  //測試點在有向邊右邊 有向邊向下穿過
    }
    return wn!=0;
}

int main()
{
    while(~scanf("%d",&n))
    {
        for(int i=1;i<=n;i++) scanf("%lf %lf",&polygon[i].x,&polygon[i].y);
        Point test;
        scanf("%d",&m);
        while(m--)
        {
            scanf("%lf %lf",&test.x,&test.y);
            if(InPolygon(test)) printf("Yes\n");
            else printf("No\n");
        }
    }
    return 0;
}

改進弧長法

時間複雜度:O(n) 適用範圍:任意多邊形
該算法感覺是轉角法的另一種優化,也解決了傳統轉角法的精度問題,也要求多邊形點給出的順序。

算法思想:
以被測點O爲座標原點,將平面劃分爲4個象限,對每個多邊形頂點P[i],計算其所在的象限,然後順序訪問多邊形的各個頂點P[i],分析P[i]和P[i+1],有下列三種情況:
1. P[i+1]在P[i]的下一象限。此時弧長和加π/2 ;(代碼中+1)
2. P[i+1]在P[i]的上一象限。此時弧長和減π/2 ;(代碼中-1)
3. P[i+1]在Pi的相對象限。利用叉積res=OP[i]xOP[i+1]計算OP[i]與OP[i+1]的關係。
若f=0,OP[i]與OP[i+1]共線,點在多邊形邊上;若f<0,OP[i]在OP[i+1]逆時針方向,弧長和減π (代碼中-2);若f>0,OP[i]在OP[i+1]順時針方向,弧長和加π (代碼中+2)。

有點累,逆時針的代碼自己畫圖推一推吧,

//判斷點P在多邊形內-改進弧長法(多邊形點順時針給出)
//用這個還不如用上一個轉角法
bool InPolygon(Point P)
{
    int q1,q2,ans=0;
    Point P1,P2;
    Vector V1,V2;
    for(int i=1,j=n;i<=n;j=i++)
    {
        P1 = polygon[j];
        P2 = polygon[i];
        V1 = P1-P;
        V2 = P2-P;
        if(OnSegment(P1,P2,P)) return true;
        q1 = V1.x>0 ? (V1.y>0 ? 0:3) : (V1.y>0 ? 1:2);
        q2 = V2.x>0 ? (V2.y>0 ? 0:3) : (V2.y>0 ? 1:2);
        int g = (q2-q1+4)%4;
        if(g==1) ans--; //在上一象限
        if(g==3) ans++; //在下一象限
        if(g==2) dcmp(V1^V2)>0 ? (ans-=2) : (ans+=2); //在相對象限
    }
    return ans!=0;
}

hdu1756-改進弧長法代碼

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const double eps = 1e-6;
const double PI = acos(-1);

int n,m;

struct Point{
    double x,y;
    Point(double x=0,double y=0):x(x),y(y){}

    //向量+
    Point operator +(const Point &b)const
    {
        return Point(x+b.x,y+b.y);
    }
    //向量-
    Point operator -(const Point &b)const
    {
        return Point(x-b.x,y-b.y);
    }
    //點積
    double operator *(const Point &b)const
    {
        return x*b.x + y*b.y;
    }
    //叉積
    //P^Q>0,P在Q的順時針方向;<0,P在Q的逆時針方向;=0,P,Q共線,可能同向或反向
    double operator ^(const Point &b)const
    {
        return x*b.y - b.x*y;
    }
}polygon[105];
typedef Point Vector;

//三態函數,判斷兩個double在eps精度下的大小關係
int dcmp(double x)
{
    if(fabs(x)<eps) return 0;
    else
        return x<0?-1:1;
}

//判斷點Q是否在P1和P2的線段上
bool OnSegment(Point P1,Point P2,Point Q)
{
    return dcmp((P1-Q)^(P2-Q))==0&&dcmp((P1-Q)*(P2-Q))<=0;
}

//判斷點P在多邊形內-改進弧長法
bool InPolygon(Point P)
{
    int q1,q2,ans=0;
    Point P1,P2;
    Vector V1,V2;
    for(int i=1,j=n;i<=n;j=i++)
    {
        P1 = polygon[j];
        P2 = polygon[i];
        V1 = P1-P;
        V2 = P2-P;
        if(OnSegment(P1,P2,P)) return true;
        q1 = V1.x>0 ? (V1.y>0 ? 0:3) : (V1.y>0 ? 1:2);
        q2 = V2.x>0 ? (V2.y>0 ? 0:3) : (V2.y>0 ? 1:2);
        int g = (q2-q1+4)%4;
        if(g==1) ans--; //在上一象限
        if(g==3) ans++; //在下一象限
        if(g==2) dcmp(V1^V2)>0 ? (ans-=2) : (ans+=2);
    }
    return ans!=0;
}

int main()
{
    while(~scanf("%d",&n))
    {
        for(int i=1;i<=n;i++) scanf("%lf %lf",&polygon[i].x,&polygon[i].y);
        Point test;
        scanf("%d",&m);
        while(m--)
        {
            scanf("%lf %lf",&test.x,&test.y);
            if(InPolygon(test)) printf("Yes\n");
            else printf("No\n");
        }
    }
    return 0;
}

叉積(點線)判斷法

時間複雜度:O(n) 適用範圍:凸多邊形

我覺得該算法只作爲了解吧。
對於多邊形(正向,即逆時針),如果一個點它的所有有向邊的左邊,那麼這個點一定在多邊形內部。利用叉積正好可以判斷點與給定邊的關係,即點是在邊的左邊右邊還是邊上。
這裏說一下,(P叉乘Q)P^Q>0說明P在Q的順時針方向,<0說明P在Q的逆時針方向,=0說明P和Q共線。

沒有代碼,我覺得只要一下就行了,不推薦。

面積法

時間複雜度:O(n)(但是時間應該會比之前的O(n)的長一點)
適用範圍:凸多邊形

瞭解即可,有精度要求,強調多邊形點給出的方向(逆時針)。
算法思想:
如果點在多邊形內部或者邊上,那麼點與多邊形所有邊組成的三角形面積和等於多邊形面積。多邊形的面積可以用叉積計算即連接座標原點和各頂點形成向量,所有向量叉積的0.5的和即爲多邊形面積。不過計算面積是會有一定誤差的,需要設置精度的誤差範圍。

沒有代碼。

二分

時間複雜度:O(logn ) 適用範圍:凸多邊形
強掉多邊形給出的方向。

這種方法以hrbust1429爲例。 題目鏈接
這道題的題意是已知構成凸多邊形A的n個點的座標,和點集B的m個點的座標,求這B的m個點是否都在凸多邊形A內(嚴格內部,就是點不能在多邊形邊上)。

算法思想:
1. 選擇多邊形其中一個點爲起點,連接其它點作射線。


2. 判斷給定的點是否在所有射線包圍的區域之內,即判斷給定點是否在最左側射線的左邊,或者在最右側射線的右邊。
3. 如果在射線包圍的區域之內,選擇構成最兩側的射線的點爲left和right,則mid = (left+right)/2,連接給頂點和起點作射線,判斷該射線在mid點和起點的哪一邊,不斷循環,如此用二分法最後求出給定點所在的三角形區域,由此確定了除起點外的一條邊。

4. 判斷給定點在這條邊的左方還是右方,由此判斷給定點是否在三角形區域內,也就是是否在多邊形內。

//判斷點P在多邊形內-O(logn) 順時針給出
bool InPolygon(Point P)
{
    if(dcmp((polygon[n]-polygon[1])^(P-polygon[1]))<=0 || dcmp((polygon[2]-polygon[1])^(P-polygon[1]))>=0) //判斷在不在區域外
        return false;
    int l=2,r=n,mid;
    //找出所在三角形區域的左邊的頂點
    while(l<r)
    {
        mid = (l+r+1)>>1;
        if(dcmp((polygon[mid]-polygon[1])^(P-polygon[1]))<=0) l=mid;
        else r = mid-1;
    } 
    //判斷在多邊形外或線上
    if(dcmp((polygon[l+1]-polygon[l])^(P-polygon[l]))>=0) return false;
    return true;
}

hrbust1429

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const double eps = 1e-6;
const double PI = acos(-1);

const int maxn = 1e5+5;
int n,m;

struct Point{
    double x,y;
    Point(double x=0,double y=0):x(x),y(y){}

    //向量+
    Point operator +(const Point &b)const
    {
        return Point(x+b.x,y+b.y);
    }
    //向量-
    Point operator -(const Point &b)const
    {
        return Point(x-b.x,y-b.y);
    }
    //點積
    double operator *(const Point &b)const
    {
        return x*b.x + y*b.y;
    }
    //叉積
    //P^Q>0,P在Q的順時針方向;<0,P在Q的逆時針方向;=0,P,Q共線,可能同向或反向
    double operator ^(const Point &b)const
    {
        return x*b.y - b.x*y;
    }
}polygon[maxn];
typedef Point Vector;

//三態函數,判斷兩個double在eps精度下的大小關係
int dcmp(double x)
{
    if(fabs(x)<eps) return 0;
    else
        return x<0?-1:1;
}

//判斷點Q是否在P1和P2的線段上
bool OnSegment(Point P1,Point P2,Point Q)
{
    return dcmp((P1-Q)^(P2-Q))==0&&dcmp((P1-Q)*(P2-Q))<=0;
}

//判斷點P在多邊形內-O(logn) 順時針給出
bool InPolygon(Point P)
{
    if(dcmp((polygon[n]-polygon[1])^(P-polygon[1]))<=0 || dcmp((polygon[2]-polygon[1])^(P-polygon[1]))>=0)
        return false;
    int l=2,r=n,mid;
    while(l<r)
    {
        mid = (l+r+1)>>1;
        if(dcmp((polygon[mid]-polygon[1])^(P-polygon[1]))<=0) l=mid;
        else r = mid-1;
    }
    if(dcmp((polygon[l+1]-polygon[l])^(P-polygon[l]))>=0) return false;
    return true;
}

int main()
{
    while(~scanf("%d",&n))
    {
        for(int i=1;i<=n;i++) scanf("%lf %lf",&polygon[i].x,&polygon[i].y);
        Point test;
        bool flag = true;
        scanf("%d",&m);
        while(m--)
        {
            scanf("%lf %lf",&test.x,&test.y);
            if(!flag) continue;
            if(!InPolygon(test)) flag = false;
        }
        if(flag) printf("YES\n");
        else printf("NO\n");
    }
    return 0;
}

最後總結一下O(n)算法的選擇:個人感覺最優先考慮射線,其次是轉角,其它的能不用最好不用。

如果對你有所幫助請給個贊,沒有也請不要踩。。。QAQ

參考博客:
二分法參考博客

轉角法參考博客

面積法 改進弧長法參考博客

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