射线法(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;
    }
posted @ 2019-04-04 11:22  kongbursi  阅读(4234)  评论(0编辑  收藏  举报