射线法(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
一、比如说,我就随便涂了一个多边形和一个点,现在我要给出一种通用的方法来判断这个点是不是在多边形内部(别告诉我用肉眼观察……)。
首先想到的一个解法是从这个点做一条射线,计算它跟多边形边界的交点个数,如果交点个数为奇数,那么点在多边形内部,否则点在多边形外。
这个结论很简单,那它是怎么来的?下面就简单讲解一下。
首先,对于平面内任意闭合曲线,我们都可以直观地认为,曲线把平面分割成了内、外两部分,其中“内”就是我们所谓的多边形区域。
基于这一认识,对于平面内任意一条直线,我们可以得出下面这些结论:
- 直线穿越多边形边界时,有且只有两种情况:进入多边形或穿出多边形。
- 在不考虑非欧空间的情况下,直线不可能从内部再次进入多边形,或从外部再次穿出多边形,即连续两次穿越边界的情况必然成对。
- 直线可以无限延伸,而闭合曲线包围的区域是有限的,因此最后一次穿越多边形边界,一定是穿出多边形,到达外部。
现在回到我们最初的题目。假如我们从一个给定的点做射线,还可以得出下面两条结论:
- 如果点在多边形内部,射线第一次穿越边界一定是穿出多边形。
- 如果点在多边形外部,射线第一次穿越边界一定是进入多边形。
把上面这些结论综合起来,我们可以归纳出:
- 当射线穿越多边形边界的次数为偶数时,所有第偶数次(包括最后一次)穿越都是穿出,因此所有第奇数次(包括第一次)穿越为穿入,由此可推断点在多边形外部。
- 当射线穿越多边形边界的次数为奇数时,所有第奇数次(包括第一次和最后一次)穿越都是穿出,由此可推断点在多边形内部。
到这里,我们已经了解了这个解法的思路,大家可以试着自己写一个实现出来。关于算法实现中某些具体问题和边界条件的处理,下次接着写,这次画图已经画够了……
后记:给出这个解法后,我简单搜了一下,原来这种算法就叫做射线法(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;
}