【数据结构和算法设计】算法篇(10) 计算几何

计算几何作为计算机科学中的一个分支,主要研究解决几何问题的算法,在计算机图形学、科学计算可视化、图形用户界面等领域,都有广泛的应用。这里以二维空间为例,讨论在计算几何中常用的算法设计方法。


10.1 向量运算

在二维空间(即平面上)中,每个输入对象都用一组点 { p 1 , p 2 , … , p n } \{ p_1, p_2, \dots, p_n\} {p1,p2,,pn} 来表示,其中每个 p i = ( x i , y i ) p_i = (x_i, y_i) pi=(xi,yi) x i , y i x_i, y_i xi,yi 分别是点 p i p_i pi 的行、列坐标,用实数表示。设计点类 Point ,后面分别讨论这些友元函数的设计:

// 计算几何: 点类
class Point {
public:
	double x;													// 行坐标
	double y;													// 列坐标
	Point() {}													// 默认构造函数
	Point(double x1, double y1) {								// 重载构造函数
		x = x1;
		y = y1;
	}
	void disp() {											  	// 输出函数
		printf("(%g, %g)", x, y); 
	}
	friend bool operator==(const Point &p1, const Point &p2); 	// 重载==运算符
	friend Point operator+(const Point &p1, const Point &p2);  	// 重载+运算符
	friend Point operator-(const Point &p1, const Point &p2);   // 重载-运算符
	
	friend double Dot(const Point &p1, const Point &p2);		// 两个向量的点积
	friend double Length(Point &p);								// 求向量长度
	friend int Angle(Point p0, Point p1, Point p2); 			// 求两线段p0p1和p0p2的夹角
	
	friend double Det(Point p1, Point p2);						// 两个向量的叉积
	friend double Direction(Point p0, Point p1, Point p2); 		// 判断两线段p0p1和p0p2的方向
	friend double Distance(Point p1, Point p2);					// 求两个点的距离
	friend double DistPtoSegment(Point p0, Point p1, Point p2); // 求p0到p1p2线段的距离
	
	friend bool InRectAngle(Point p0, Point p1, Point p2);		// 判断点p0是否在p1和p2表示的矩阵中
	friend bool OnSegment(Point p0, Point p1, Point p2); 		// 判断点p0是否在p1p2线段上
	friend bool Parallel(Point p1, Point p2, Point p3, Point p4); // 判断p1p2和p3p4线段是否平行
	friend bool SegIntersect(Point p1, Point p2, Point p3, Point p4); // 判断p1p2和p3p4线段是否相交
	friend bool PointInPolygon(Point p0, vector<Point> a); 		// 判断点p0是否在点集a所形成的多边形内
};

bool operator==(const Point &p1, const Point &p2) {				// 重载==运算符
	return p1.x == p2.x && p1.y == p2.y;
}

线段是直线在两个定点之间(包含这两个点)的部分,可通过两个点 p 1 , p 2 p_1, p_2 p1,p2 来表示(向量也是)。通常线段是有向的,有向线段 p 1 p 2 p_1p_2 p1p2 是从起点 p 1 p_1 p1 到终点 p 2 p_2 p2这种既有大小又有方向的量称为向量 vector p 1 p 2 p_1p_2 p1p2 向量的长度或模,为点 p 1 p_1 p1 p 2 p_2 p2 的距离,记为 ∣ p 1 p 2 ∣ |p_1p_2| p1p2 。这里默认将一个点 p p p 看成是坐标原点为 ( 0 , 0 ) (0, 0) (0,0) 的向量 p p p

10.1.1 向量的基本运算

1. 向量加减

对于两个点表示的两个向量 p 1 , p 2 p_1, p_2 p1,p2(起点均为原点 ( 0 , 0 ) (0, 0) (0,0) ),向量加法定义为 p 1 + p 2 = ( p 1. x + p 2. x ,   p 1. y + p 2. y ) p_1 + p_2 = (p1.x + p2.x,\ p1.y+p2.y) p1+p2=(p1.x+p2.x, p1.y+p2.y) 其结果仍为一个向量。

向量加法一般可用平行四边形法则,如图10.1所示,两个向量为 p 1 ( 2 , − 1 ) ,   p 2 ( 3 , 3 ) p_1(2, -1),\ p_2(3, 3) p1(2,1), p2(3,3) ,则 p 3 = p 1 + p 2 = ( 5 , 2 ) p_3 = p_1 + p_2 = (5, 2) p3=p1+p2=(5,2)
在这里插入图片描述
求两个向量 p 1 , p 2 p_1, p_2 p1,p2 的加法运算的算法如下:

Point operator+(const Point &p1, const Point &p2) { 			// 重载+运算符
	return Point(p1.x + p2.x, p1.y + p2.y);
}

向量减法是向量加法的逆运算,一个向量减去另一个向量等于加上那个向量的负向量,即 p 1 − p 2 = p 1 + ( − p 2 ) = ( p 1 . x − p 2 . x , p 1 . y − p 2 . y ) p_1 - p_2 = p_1 + (-p_2) = (p_1.x - p_2.x, p_1.y - p_2.y) p1p2=p1+(p2)=(p1.xp2.x,p1.yp2.y)

Point operator-(const Point &p1, const Point &p2) {				// 重载-运算符
	return Point(p1.x - p2.x, p1.y - p2.y);
}

显然有性质 p 1 + p 2 = p 2 + p 1 ,   p 1 − p 2 = − ( p 2 − p 1 ) p_1 + p_2 = p_2 + p_1,\ p_1 - p_2 = -(p_2 - p_1) p1+p2=p2+p1, p1p2=(p2p1)

如图10.2所示,两个向量为 p 1 ( 2 , − 1 ) ,   p 2 ( 5 , 4 ) p_1(2, -1),\ p_2(5,4) p1(2,1), p2(5,4) ,则 p 3 = p 2 − p 1 = ( 3 , 5 ) p_3 = p_2 - p_1 = (3, 5) p3=p2p1=(3,5) ,将 p 3 p_3 p3 平移到 p 1 p 2 p_1 p_2 p1p2 处(虚线),可看出 p 3 p_3 p3 的长度与 p 1 , p 2 p_1, p_2 p1,p2 连接线的长度相同、方向相同。用 ∣ p ∣ |p| p 表示向量 p p p 的长度,有 ∣ p 2 − p 1 ∣ = |p_2 - p_1| = p2p1= p 1 p_1 p1 p 2 p_2 p2 的长度。实际上, p 2 − p 1 p_2 - p_1 p2p1 向量可看成是以 p 1 p_1 p1 为原点的 p 2 p_2 p2 向量。

2. 向量点积运算

两个向量 p 1 , p 2 p_1, p_2 p1,p2 的点积(或內积),定义为 p 1 ⋅ p 2 = ∣ p 1 ∣ × ∣ p 1 ∣ × cos ⁡ θ = p 1 . x × p 2 . x + p 1 . y × p 2 . y p_1 \cdot p_2 = |p_1| \times |p_1| \times \cos \theta = p_1.x \times p_2.x + p_1.y \times p_2.y p1p2=p1×p1×cosθ=p1.x×p2.x+p1.y×p2.y

其结果是一个标量,其中向量的长度 ∣ p ∣ = p . x 2 + p . y 2 |p| = \sqrt{ p.x^2 +p.y^2} p=p.x2+p.y2 θ \theta θ 表示两个向量的夹角,如图10.3所示。显然有性质 p 1 ⋅ p 2 = p 2 ⋅ p 1 p_1 \cdot p_2 = p_2 \cdot p_1 p1p2=p2p1
在这里插入图片描述
求两个向量 p 1 p_1 p1 p 2 p_2 p2 点积的算法如下:

double Dot(const Point &p1, const Point &p2) {					// 两个向量的点积		
	return p1.x * p2.x + p1.y * p2.y;
}

利用点积,求一个向量的长度的算法如下:

double Length(const Point &p) {									// 求向量长度
	return sqrt(Dot(p, p));
}

可以通过点积的符号,判断两向量相互之间的夹角关系

  • p 1 ⋅ p 2 > 0 p_1 \cdot p_2 > 0 p1p2>0 ,则向量 p 1 p_1 p1 p 2 p_2 p2 之间的夹角为锐角;
  • p 1 ⋅ p 2 = 0 p_1\cdot p_2 = 0 p1p2=0 ,则向量 p 1 p_1 p1 p 2 p_2 p2 之间的夹角为直角;
  • p 1 ⋅ p 2 < 0 p_1\cdot p_2 < 0 p1p2<0 ,则向量 p 1 p_1 p1 p 2 p_2 p2 之间的夹角为钝角。

对于具有公共起点的两个线段 p 0 p 1 p_0p_1 p0p1 p 0 p 2 p_0p_2 p0p2 ,只需要把 p 0 p_0 p0 作为原点就可以,即 p 1 − p 0 p_1 - p_0 p1p0 p 2 − p 0 p_2 - p_0 p2p0 都是向量,它们的点积为 r = ( p 1 − p 0 ) ⋅ ( p 2 − p 0 ) r = (p_1 - p_0) \cdot (p_2 - p_0) r=(p1p0)(p2p0) 。利用上述关系,求两条线段 p 0 p 1 p_0p_1 p0p1 p 0 p 2 p_0p_2 p0p2 的夹角的算法如下:

int Angle(Point p0, Point p1, Point p2) { 						// 求两线段p0p1和p0p2的夹角
	double d = Dot(p1 - p0, p2 - p0);
	if (d == 0) return 0;		// 两线段p1p0和p2p0的夹角为直角
	else if (d > 0) return 1; 	// 两线段p1p0和p2p0的夹角为锐角
	else return -1;				// 两线段p1p0和p2p0的夹角为钝角
}

3. 向量叉积运算

两个向量 p 1 p_1 p1 p 2 p_2 p2 的叉积(外积) p 1 × p 2 = ∣ p 1 ∣ × ∣ p 2 ∣ × sin ⁡ θ = p 1 . x × p 2 . y − p 2 . x × p 1 . y p_1 \times p_2 = |p_1| \times |p_2| \times \sin\theta = p_1.x \times p_2.y - p_2.x \times p_1.y p1×p2=p1×p2×sinθ=p1.x×p2.yp2.x×p1.y ,其结果是一个标量,其中 θ \theta θ 表示两个向量的夹角,如图10.4所示。显然有性质 p 1 × p 2 = − p 2 × p 1 p_1 \times p_2 = -p_2 \times p_1 p1×p2=p2×p1

求两个向量 p 1 , p 2 p_1, p_2 p1,p2 叉积的算法如下:

double Det(const Point &p1, const Point &p2) {					// 两个向量的叉积
	return p1.x * p2.y - p1.y * p2.x;
}

向量叉积的计算是关于线段算法的核心,如图10.4所示,叉积 p 1 × p 2 p_1 \times p_2 p1×p2 可以看作是由 ( 0 , 0 ) ,   p 1 ,   p 2 ,   p 1 + p 2 (0, 0),\ p_1, \ p_2,\ p_1+p_2 (0,0), p1, p2, p1+p2 所组成的平行四边形的带符号的面积,当 p 1 × p 2 p_1 \times p_2 p1×p2 值为正时,向量 p 1 p_1 p1 可沿着平行四边形内部逆时针旋转到达 p 2 p_2 p2 ;当 p 1 × p 2 p_1 \times p_2 p1×p2 值为负时,向量 p 1 p_1 p1 可沿着平行四边形内部顺时针旋转到达 p 2 p_2 p2可通过叉积的符号,判断两个向量相互之间的顺逆时针关系

  • p 1 × p 2 > 0 p_1 \times p_2 > 0 p1×p2>0 ,则 p 1 p_1 p1 p 2 p_2 p2 的顺时针方向(图10.4、图10.5 (a)所示);
  • p 1 × p 2 < 0 p_1 \times p_2 < 0 p1×p2<0 ,则 p 1 p_1 p1 p 2 p_2 p2 的逆时针方向(图10.5 (b)所示);
  • p 1 × p 2 = 0 p_1 \times p_2 = 0 p1×p2=0 ,则 p 1 p_1 p1 p 2 p_2 p2 共线,但可能同向、也可能反向。

对于具有公共起点的两个线段 p 0 p 1 p_0p_1 p0p1 p 0 p 2 p_0p_2 p0p2 ,只需要把 p 0 p_0 p0 作为原点就可以进行向量叉积运算,即 p 1 − p 0 p_1 - p_0 p1p0 p 2 − p 0 p_2 - p_0 p2p0 都是向量,它们的叉积为 r = ( p 1 − p 0 ) × ( p 2 − p 0 ) = ( p 1 . x − p 0 . x ) × ( p 2 . y − p 0 . y ) − ( p 2 . x − p 0 . x ) × ( p 1 . y − p 0 . y ) r = (p_1 - p_0) \times (p_2 - p_0) = (p_1.x - p_0.x) \times (p_2.y - p_0.y) - (p_2.x - p_0.x) \times (p_1.y - p_0.y) r=(p1p0)×(p2p0)=(p1.xp0.x)×(p2.yp0.y)(p2.xp0.x)×(p1.yp0.y) ,可通过该叉积的符号与上述说明,判断两线段相互之间的顺逆时针关系(超过 180 ° 180\degree 180° 就会产生不同的顺逆时针关系)。
在这里插入图片描述
判断两条线段 p 0 p 1 p_0p_1 p0p1 p 0 p 2 p_0p_2 p0p2 方向的算法如下:

double Direction(Point p0, Point p1, Point p2) { 				// 判断两线段p0p1和p0p2的方向
	double d = Det(p1 - p0, p2 - p0); 
	if (d == 0) return 0;	    // p0,p1,p2三点共线
	else if (d > 0) return 1;   // p0p1在p0p2的顺时针方向上
	else return -1;			    // p0p1在p0p2的逆时针方向上
}

4. 两个点的距离

两个点 p 1 , p 2 p_1, p_2 p1,p2 之间的距离为 ( p 1 . x − p 2 . x ) 2 + ( p 1 . y − p 2 . y ) 2 \sqrt{(p_1.x - p_2.x)^2 + (p_1.y - p_2.y)^2} (p1.xp2.x)2+(p1.yp2.y)2 。可将 p 1 , p 2 p_1,p_2 p1,p2 视为一个向量,(用 Length 函数即內积)求其长度;或直接写出数学公式:

double Distance(Point p1, Point p2) {							// 求两个点的距离
	return sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y));
}

5. 点到线段的距离

求点 p 0 p_0 p0 到线段 p 1 p 2 p_1p_2 p1p2 的距离。设 p 0 p_0 p0 在线段 p 1 p 2 p_1p_2 p1p2 上的投影点为 q q q ,设向量 v 1 = p 2 − p 1 ,   v 2 = p 1 − p 2 ,   v 3 = p 0 − p 1 ,   v 4 = p 0 − p 2 v_1 = p_2 - p_1,\ v_2 = p_1 - p_2,\ v_3 = p_0 - p_1,\ v_4 = p_0 - p_2 v1=p2p1, v2=p1p2, v3=p0p1, v4=p0p2 。点 q q q 的三种可能情况如图10.6所示。

  • 若满足图10.6 a)所示的情况, p 0 p_0 p0 到线段 p 1 p 2 p_1p_2 p1p2 的距离为向量 v 3 v_3 v3 的长度;
  • 若满足图10.6 b)所示的情况, p 0 p_0 p0 到线段 p 1 p 2 p_1p_2 p1p2 的距离为向量 v 4 v_4 v4 的长度;
  • 若满足图10.6 c)所示的情况, p 0 p_0 p0 到线段 p 1 p 2 p_1p_2 p1p2 的距离为向量 v 1 v_1 v1 v 3 v_3 v3 叉积的绝对值(平行四边形的面积)除以底长。可设 v 1 , v 3 v_1, v_3 v1,v3 的夹角为 θ \theta θ ,则 sin ⁡ θ = ∣ p 0 q ∣ ∣ p 0 p 1 ∣ \sin \theta = \dfrac{ | p_0q|} { | p_0 p_1| } sinθ=p0p1p0q ,则 ∣ p 0 q ∣ = sin ⁡ θ ⋅ ∣ p 0 p 1 ∣ | p_0 q| =\sin \theta \cdot |p_0p_1| p0q=sinθp0p1 ,即 v 1 × v 3 v_1 \times v_3 v1×v3 的绝对值除以 ∣ p 1 p 2 ∣ |p_1p_2| p1p2
    在这里插入图片描述

对应的算法如下:

double DistPtoSegment(Point p0, Point p1, Point p2) { 			// 求p0到p1p2线段的距离
	Point v1 = p2 - p1, v2 = p1 - p2, v3 = p0 - p1, v4 = p0 - p2;
	if (p1 == p2) return Length(p0 - p1); // 两点重合
	if (Dot(v1, v3) < 0)
		return Length(v3);
	else if (Dot(v2, v4) < 0) 
		return Length(v4);
	else 
		return fabs(Det(v1, v3)) / Length(v1);
}

10.1.2 判断一个点是否在一个矩形内

设一个矩形的左上角为点 p 1 p_1 p1 、右下角为点 p 2 p_2 p2 ,另有一个点 p 0 p_0 p0 ,现要判断该点是否在指定的矩形内。

p 0 p 1 p_0p_1 p0p1 p 0 p 2 p_0p_2 p0p2 看成是具有公共起点的两个线段,把 p 0 p_0 p0 作为原点,显然 p 0 p 1 p_0p_1 p0p1 p 0 p 2 p_0p_2 p0p2 两线段的夹角 θ \theta θ 为直角或钝角时,点 p 0 p_0 p0 就落在该矩形内(含点 p 1 , p 2 p_1, p_2 p1,p2 ),如图10.7所示。所以,点 p 0 p_0 p0 在该矩形内应满足条件 ( p 1 − p 0 ) ⋅ ( p 2 − p 0 ) ≤ 0 (p_1 - p_0) \cdot (p_2 - p_0) \le 0 (p1p0)(p2p0)0
在这里插入图片描述
对应的判断算法如下:

bool InRectAngle(Point p0, Point p1, Point p2) {				// 判断点p0是否在p1和p2表示的矩阵中
	return Dot(p1 - p0, p2 - p0) <= 0;
}

另一种更直观的判断方法是, p 0 p_0 p0 在该矩形内应满足以下条件:

min(p1.x, p2.x) <= p0.x <= max(p1.x, p2.x) &&
min(p1.y, p2.y) <= p0.y <= max(p1.y, p2.y)

10.1.3 判断一个点是否在一条线段上

设点为 p 0 p_0 p0 、线段为 p 1 p 2 p_1p_2 p1p2若点 p 0 p_0 p0 在该线段上(含点 p 1 , p 2 p_1, p_2 p1,p2 ),应同时满足两个条件:一是点 p 0 p_0 p0 在线段 p 1 p 2 p_1p_2 p1p2 所在的直线上;二是点 p 0 p_0 p0 在以 p 1 , p 2 p_1, p_2 p1,p2 为对角顶点的矩形内。前者保证点 p 0 p_0 p0 在直线 p 1 p 2 p_1p_2 p1p2 上,后者保证点 p 0 p_0 p0 不在线段 p 1 p 2 p_1p_2 p1p2 的延长线或反向延长线上。

  • p 0 p_0 p0 在线段 p 1 p 2 p_1p_2 p1p2 所在的直线上,应满足的条件是 ( p 1 − p 0 ) × ( p 2 − p 0 ) = 0 (p_1 - p_0) \times (p_2 - p_0) = 0 (p1p0)×(p2p0)=0
  • p 0 p_0 p0 在以 p 1 , p 2 p_1, p_2 p1,p2 为对角顶点的矩形内,应满足的条件是 ( p 1 − p 0 ) ⋅ ( p 2 − p 0 ) ≤ 0 (p_1 - p_0) \cdot (p_2 - p_0) \le 0 (p1p0)(p2p0)0
bool OnSegment(Point p0, Point p1, Point p2) { 					// 判断点p0是否在p1p2线段上
	return Det(p1 - p0, p2 - p0) == 0 && Dot(p1 - p0, p2 - p0) <= 0;
}

10.1.4 判断两条线段是否平行

设两条线段为 p 1 p 2 p_1p_2 p1p2 p 3 p 4 p_3p_4 p3p4如果它们的夹角为零,则是平行的,所以两条线段 p 1 p 2 p_1p_2 p1p2 p 3 p 4 p_3p_4 p3p4 平行应满足的条件是 ( p 2 − p 1 ) × ( p 4 − p 3 ) = 0 (p_2 - p_1) \times (p_4 - p_3) = 0 (p2p1)×(p4p3)=0

bool Parallel(Point p1, Point p2, Point p3, Point p4) {			// 判断p1p2和p3p4线段是否平行
	return Det(p2 - p1, p4 - p3) == 0;
}

10.1.5 判断两条线段是否相交

对于两条直线,可以不平行、即相交,但对线段则不然。设两条线段为 p 1 p 2 p_1p_2 p1p2 p 3 p 4 p_3p_4 p3p4 ,如图10.8所示,要判断它们是否相交(包括端点),只要点 p 1 , p 2 p_1, p_2 p1,p2 在线段 p 3 p 4 p_3p_4 p3p4 的两边且点 p 3 , p 4 p_3, p_4 p3,p4 在线段 p 1 p 2 p_1p_2 p1p2 的两边,那么这两条线段必然相交

那么如何判断两点是否在一条线段的两边呢?令:

  • d 1 = p 3 p 1 × p 3 p 4 d_1 = p_3 p_1 \times p_3 p_4 d1=p3p1×p3p4 等于 Direction(p3, p1, p4) ,求 p 3 p 1 p_3p_1 p3p1 p 3 p 4 p_3p_4 p3p4 的哪个方向上;
  • d 2 = p 3 p 2 × p 3 p 4 d_2 = p_3 p_2 \times p_3 p_4 d2=p3p2×p3p4 等于 Direction(p3, p2, p4) ,求 p 3 p 2 p_3p_2 p3p2 p 3 p 4 p_3p_4 p3p4 的哪个方向上;
  • d 3 = p 1 p 3 × p 1 p 2 d_3 = p_1 p_3 \times p_1 p_2 d3=p1p3×p1p2 等于 Direction(p1, p3, p2) ,求 p 1 p 3 p_1p_3 p1p3 p 1 p 2 p_1p_2 p1p2 的哪个方向上;
  • d 4 = p 1 p 4 × p 1 p 2 d_4 = p_1 p_4 \times p_1 p_2 d4=p1p4×p1p2 等于 Direction(p1, p4, p2) ,求 p 1 p 4 p_1p_4 p1p4 p 1 p 2 p_1p_2 p1p2 的哪个方向上。

这两条线段相交的情况如下:

  1. d 1 < 0 d_1 < 0 d1<0 p 3 p 1 p_3p_1 p3p1 p 3 p 4 p_3p_4 p3p4 的逆时针方向上)且 d 2 > 0 d_2 > 0 d2>0 p 3 p 2 p_3p_2 p3p2 p 3 p 4 p_3p_4 p3p4 的顺时针方向上),图10.8就是这种情况;
  2. d 1 > 0 d_1 > 0 d1>0 p 3 p 1 p_3p_1 p3p1 p 3 p 4 p_3p_4 p3p4 的顺时针方向上)且 d 2 < 0 d_2 < 0 d2<0 p 3 p 2 p_3p_2 p3p2 p 3 p 4 p_3p_4 p3p4 的逆时针方向上),图10.8中的 p 1 , p 2 p_1, p_2 p1,p2 交换就是这种情况。
    在这里插入图片描述

上述两种情况表示 p 1 , p 2 p_1, p_2 p1,p2 两个点在线段 p 3 p 4 p_3p_4 p3p4 的两边,即条件为 d 1 × d 2 < 0 d_1 \times d_2 < 0 d1×d2<0 。同理,若有 d 3 × d 4 < 0 d_3 \times d_4 < 0 d3×d4<0 ,则 p 3 , p 4 p_3, p_4 p3,p4 两个点在线段 p 1 p 2 p_1p_2 p1p2 的两边。

另外,若 d i = 0   ( 1 ≤ i ≤ 4 ) d_i = 0\ (1 \le i \le 4) di=0 (1i4) ,还需要判断对应的点是否在线段上。例如,若 d 1 = 0 d_1 = 0 d1=0 ,表示 p 1 , p 3 , p 4 p_1, p_3, p_4 p1,p3,p4 三点共线,还需要判断点 p 1 p_1 p1 p 3 p 4 p_3p_4 p3p4 线段上。对应的判断算法如下:

bool SegIntersect(Point p1, Point p2, Point p3, Point p4) { 	// 判断p1p2和p3p4线段是否相交
	int d1, d2, d3, d4;
	d1 = Direction(p3, p1, p4);	// 求p3p1在p3p4的哪个方向上
	d2 = Direction(p3, p2, p4); // 求p3p2在p3p4的哪个方向上
	d3 = Direction(p1, p3, p2); // 求p1p3在p1p2的哪个方向上
	d4 = Direction(p1, p4, p2); // 求p1p4在p1p2的哪个方向上
	if (d1 * d2 < 0 && d3 * d4 < 0) return true;
	if (d1 == 0 && OnSegment(p1, p3, p4))		// 若d1为0且p1在p3p4线段上
		return true;
	else if (d2 == 0 && OnSegment(p2, p3, p4))  // 若d2为0且p2在p3p4线段上
		return true;
	else if (d3 == 0 && OnSegment(p3, p1, p2))  // 若d3为0且p3在p1p2线段上
		return true;
	else if (d4 == 0 && OnSegment(p4, p1, p2))  // 若d4为0且p4在p1p2线段上
		return true;
	else
		return false;
}

10.1.6 判断一个点是否在多边形内

一个多边形由 n n n 个顶点 a [ 0... n ] a[0...n] a[0...n] 构成( a [ n ] = a [ 0 ] a[n] = a[0] a[n]=a[0]),假设其所有的边不相交,称之为简单多边形,这里讨论的多边形默认都是简单多边形。现有一个点 p 0 p_0 p0 ,要判断点 p 0 p_0 p0 是否在该多边形内(含边界)。

其基本思想是从点 p 0 p_0 p0 引一条水平向右的射线,统计该射线与多边形相交的情况,如果相交次数是奇数,那么就在多边形内;否则在多边形外。例如,如图10.9所示,多边形由 8 8 8 个顶点构成,从点 p 0 p_0 p0 引出的射线与多边形相交的交点个数为 3 3 3 ,它在多边形内;而从点 p 1 p_1 p1 引出的射线与多边形相交的交点个数为 2 2 2 ,它在多边形外。
在这里插入图片描述

对于多边形的一条边 p 1 p 2 p_1p_2 p1p2 ,它构成的直线的方程为 y − p 1 . y = k ( x − p 1 . x ) y - p_1.y = k(x - p_1.x) yp1.y=k(xp1.x) ,其斜率 k = p 2 . y − p 1 . y p 2 . x − p 1 . x k = \dfrac{p_2.y - p_1.y} {p_2.x - p_1.x} k=p2.xp1.xp2.yp1.y ,所以有 x = y − p 1 . y k + p 1 . x = ( y − p 1 . y ) ( p 2 . x − p 1 . x ) p 2 . y − p 1 . y + p 1 . x x = \dfrac{y - p_1.y} {k} + p_1.x = \dfrac{ (y - p_1.y)(p_2.x - p_1.x) } { p_2.y - p_1.y } +p_1.x x=kyp1.y+p1.x=p2.yp1.y(yp1.y)(p2.xp1.x)+p1.x 从点 p 0 p_0 p0 引一条水平向右的射线的方程为 y = p 0 y y = p_0 y y=p0y 。如果这两条线有交点,则交点为 ( x , p 0 . y ) (x, p_0.y) (x,p0.y) ,其中 x = ( p 0 . y − p 1 . y ) ( p 2 . x − p 1 . x ) p 2 . y − p 1 . y + p 1 . x x = \dfrac{ (p_0.y - p_1.y )(p_2.x - p_1.x) } { p_2.y - p_1.y } +p_1.x x=p2.yp1.y(p0.yp1.y)(p2.xp1.x)+p1.x

注意,上述公式中这样变形,使得 p 1 p 2 p_1p_2 p1p2 为垂直线时 1 k = 0 \dfrac{1}{k} = 0 k1=0 、为水平线时 1 k = ∞ \dfrac{1}{k } = \infin k1= ,加上射线为水平线,这都要求特判 p 1 p 2 p_1p_2 p1p2 为水平线的情况

判断点 p 0 p_0 p0 是否在多边形 a [ 0... n ] a[0...n] a[0...n] 中的步骤如下:

  1. c n t = 0 cnt = 0 cnt=0 i i i 0 0 0 n n n 循环;
  2. p 1 = a [ i ] ,   p 2 = a [ i + 1 ] p_1 = a[i],\ p_2 = a[i + 1] p1=a[i], p2=a[i+1] ,若 p 0 p_0 p0 p 1 p 2 p_1p_2 p1p2 线段上,则返回 true
  3. p 1 p 2 p_1p_2 p1p2 是一条水平线,或者 p 0 p_0 p0 p 1 p 2 p_1p_2 p1p2 线段的上方或下方,则没有交点,转向下一条线段进行求解。
  4. 求出射线与线段 p 1 p 2 p_1p_2 p1p2 的交点的 x x x
  5. x > p 0 . x x > p_0.x x>p0.x ,则交点个数 cnt 加一;
  6. 循环结束后,返回 cnt % 2 == 1 值,即交点个数为奇数表示该点在多边形内。

对应的判断算法如下:

bool PointInPolygon(Point p0, vector<Point> a) { 				// 判断点p0是否在点集a所形成的多边形内
	int i, cnt = 0;
	double x;
	Point p1, p2;
	for (int i = 0; i < a.size(); ++i) {
		p1 = a[i]; p2 = a[i + 1];   // 取多边形的一条边
		if (OnSegment(p0, p1, p2))  // 如果点p0在多边形边p1p2线段上,返回true	
			return true;
		// 以下求解y=p0.y与p1p2的交点
		if (p1.y == p2.y) continue; // 如果p1p2是水平线,直接跳过
		// 以下两种情况是交点在p1p2的延长线上、而非p1p2线段上
		if (p0.y < p1.y && p0.y < p2.y) continue; 	// p0在p1p2线段下方,直接跳过
		if (p0.y >= p1.y && p0.y >= p2.y) continue; // p0在p1p2线段上方,直接跳过
		x = (p0.y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y) + p1.x; // 求交点坐标的x值
		if (x > p0.x) ++cnt; // 只统计射线的一边
	}
	return (cnt % 2 == 1);
}

10.1.7 求三个点构成的三角形的面积

对于三个顶点 p 0 , p 1 , p 2 p_0, p_1, p_2 p0,p1,p2 构成的三角形,求面积有多种计算公式。从向量的角度看, 3 3 3 个向量构成的三角形如图10.10 a)所示,可将其两条边看成以 p 0 p_0 p0 为原点的三角形,这两条边分别是 p 1 − p 0 p_1 - p_0 p1p0 p 2 − p 0 p_2 - p_0 p2p0 ,如图10.10 b)所示,则该三角形面积 S ( p 0 , p 1 , p 2 ) S(p_0, p_1, p_2) S(p0,p1,p2) 等于以 p 1 − p 0 p_1 - p_0 p1p0 p 2 − p 0 p_2 - p_0 p2p0 向量构成的平行四边形面积的一半,即 S ( p 0 , p 1 , p 2 ) = ( p 1 − p 0 ) × ( p 2 − p 0 ) 2 S(p_0, p_1, p_2) =\dfrac{ (p_1 - p_0) \times (p_2 - p_0) } {2} S(p0,p1,p2)=2(p1p0)×(p2p0)

在这里插入图片描述
( p 1 − p 0 ) × ( p 2 − p 0 ) (p_1 - p_0) \times (p_2 - p_0) (p1p0)×(p2p0) 的结果有正有负,所以 S ( p 0 , p 1 , p 2 ) = ( p 1 − p 0 ) × ( p 2 − p 0 ) / 2 S(p_0, p_1, p_2) = (p_1 - p_0) \times (p_2 - p_0) / 2 S(p0,p1,p2)=(p1p0)×(p2p0)/2 称为有向面积,实际面积为其绝对值。对应的算法如下:

double triangleArea(Point p0, Point p1, Point p2) {				// 求三边形面积
	return fabs(Det(p1 - p0, p2 - p0)) / 2;
}

根据向量叉积运算规则有:

  • ( p 1 − p 0 ) (p_1 - p_0) (p1p0) ( p 2 − p 0 ) (p_2 - p_0) (p2p0) 的顺时针方向,则 ( p 1 − p 0 ) × ( p 2 − p 0 ) > 0 (p_1 - p_0)\times (p_2 - p_0) > 0 (p1p0)×(p2p0)>0 。图10.10中就是这种情况。
  • ( p 1 − p 0 ) (p_1 - p_0) (p1p0) ( p 2 − p 0 ) (p_2 - p_0) (p2p0) 的逆时针方向,则 ( p 1 − p 0 ) × ( p 2 − p 0 ) < 0 (p_1 - p_0)\times (p_2 - p_0) < 0 (p1p0)×(p2p0)<0

10.1.8 求一个多边形的面积

若一个多边形由 n n n 个顶点构成,采用 vector<Point> p 存储,求其面积的方法有多种。常用的是采用三角形剖分的方法,取一个顶点作为剖分出的三角形的顶点,三角形的其他两个顶点为多边形上相邻的点对,如图10.11所示。
在这里插入图片描述
已知三角形的 3 3 3 个顶点向量,可通过向量叉积得到其面积,还可以通过向量叉积解决凹多边形中重复面积的计算问题。在图10.11中 7 7 7 个顶点分别是 p 0 ( 5 , 0 ) ,   p 1 ( 9 , 3 ) ,   p 2 ( 10 , 7 ) ,   p 3 ( 4 , 9 ) ,   p 4 ( 0 , 6 ) ,   p 5 ( 3 , 5 ) ,   p 6 ( 0 , 2 ) p_0(5, 0),\ p_1(9, 3),\ p_2(10, 7),\ p_3(4, 9),\ p_4(0, 6),\ p_5(3, 5),\ p_6(0, 2) p0(5,0), p1(9,3), p2(10,7), p3(4,9), p4(0,6), p5(3,5), p6(0,2) ,以 p 0 p_0 p0 为剖分点,求解过程如下:

  1. ( p 1 − p 0 ) × ( p 2 − p 0 ) / 2 = 6.5 (p_1 - p_0) \times (p_2 - p_0)/ 2 = 6.5 (p1p0)×(p2p0)/2=6.5 ,得到 S ( p 0 , p 1 , p 2 ) = 6.5 S(p_0, p_1, p_2) = 6.5 S(p0,p1,p2)=6.5 p 0 p 1 p_0p_1 p0p1 p 0 p 2 p_0p_2 p0p2 的顺时针方向;
  2. ( p 2 − p 0 ) × ( p 3 − p 0 ) / 2 = 26 (p_2 - p_0) \times (p_3 - p_0) / 2 = 26 (p2p0)×(p3p0)/2=26 ,得到 S ( p 0 , p 2 , p 3 ) = 26 S(p_0, p_2, p_3) = 26 S(p0,p2,p3)=26
  3. ( p 3 − p 0 ) × ( p 4 − p 0 ) / 2 = 19.5 (p_3 - p_0) \times (p_4 - p_0) / 2 = 19.5 (p3p0)×(p4p0)/2=19.5 ,得到 S ( p 0 , p 3 , p 4 ) = 19.5 S(p_0, p_3, p_4) = 19.5 S(p0,p3,p4)=19.5 ,含 p 4 − p 5 − x p_4 - p_5 - x p4p5x 部分(不应该包括在多边形面积中)面积和 p 0 − p 5 − x p_0 - p_5 - x p0p5x 部分面积;
  4. ( p 4 − p 0 ) × ( p 5 − p 0 ) / 2 = − 6.5 (p_4 - p_0) \times (p_5 - p_0) / 2 = -6.5 (p4p0)×(p5p0)/2=6.5 p 0 p 4 p_0p_4 p0p4 p 0 p 5 p_0p_5 p0p5 的逆时针方向),得到 S ( p 0 , p 4 , p 5 ) = − 6.5 S(p_0, p_4, p_5) = -6.5 S(p0,p4,p5)=6.5 ,其绝对值含 p 4 − p 5 − x p_4 - p_5 - x p4p5x 部分面积和 p 0 − p 5 − x p_0 - p_5 - x p0p5x 部分面积。由于为负数, S ( p 0 , p 3 , p 4 ) + S ( p 0 , p 4 , p 5 ) S(p_0, p_3, p_4) + S(p_0, p_4, p_5) S(p0,p3,p4)+S(p0,p4,p5) 恰好得到 p 0 − p 3 − p 4 − p 5 p_0 - p_3 - p_4 - p_5 p0p3p4p5 部分的面积;
  5. ( p 5 − p 0 ) × ( p 6 − p 0 ) / 2 = 10.5 (p_5 - p_0) \times (p_6 - p_0) / 2 = 10.5 (p5p0)×(p6p0)/2=10.5 ,得到 S ( p 0 , p 5 , p 6 ) = 10.5 S(p_0, p_5, p_6) = 10.5 S(p0,p5,p6)=10.5
  6. 上述所有的有向面积相加,得到多边形的面积 56 56 56

对应的算法如下:

double polyArea(vector<Point> p) { 								// 求多边形的面积
	double ans = 0.0;
	for (int i = 1; i < p.size() - 1; ++i)
		ans += Det(p[i] - p[0], p[i + 1] - p[0]);
	return fabs(ans) / 2; // 累计有向面积结果的绝对值
}

10.2 求解凸包问题

【数据结构和算法设计】算法篇(10) 计算几何(3) 凸包问题


10.3 求解最近点对问题

【数据结构和算法设计】算法篇(10) 计算几何(3) 最近点对问题


10.4 求解最远点对问题

【数据结构和算法设计】算法篇(10) 计算几何(4) 最远点对问题


10.5 其他题目

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
ACM Fighting! 2 1.计算几何 5 1.1 注意 5 1.2几何公式 6 1.3 多边形 8 1.4多边形切割 11 1.5 浮点函数 12 1.6 面积 18 1.7球面 18 1.8三角形 19 1.9三维几何 22 1.10 凸包 30 1.11 网格 32 1.12 圆 33 1.13 矢量运算求几何模板 35 1.14结构体表示几何图形 47 1.15四城部分几何模板 52 1.16 一些代码 54 1.16.1 最小圆覆盖_zju1450 54 1.16.2 直线旋转_两凸包的最短距离(poj3608) 58 1.16.3 扇形的重心 62 1.16.4 根据经度纬度求球面距离 62 1.16.5 多边形的重心 64 1.16.6 存不存在一个平面把两堆点分开(poj3643) 66 1.16.7 pku_3335_判断多边形的核是否存在 67 1.16.8 pku_2600_二分+圆的参数方程 74 1.16.9 pku_1151_矩形相交的面积 76 1.16.10 pku_1118_共线最多的点的个数 78 1.16.11 pku2826_线段围成的区域可储水量 80 1.16.12 Pick公式 84 1.16.13 N点中三个点组成三角形面积最大 86 1.16.14 直线关于圆的反射 89 1.16.15 pku2002_3432_N个点最多组成多少个正方形(hao) 94 1.16.16 pku1981_单位圆覆盖最多点(poj1981)CircleandPoints 97 1.16.17 pku3668_GameofLine_N个点最多确定多少互不平行的直线(poj3668) 99 1.16.18 求凸多边形直径 100 2.组合 102 2.1 组合公式 102 2.2 排列组合生成 102 2.3 生成gray码 104 2.4 置换(polya) 104 2.5 字典序全排列 105 2.6 字典序组合 105 2.7 一些原理及其例子 106 3.数论 108 3.1 阶乘最后非0位 108 3.2 模线性方程组 108 3.3 素数 110 3.4 欧拉函数 114 3.6高精度 116 3.6.1平方根 116 3.6.2 高精度乘幂 117 3.7 高斯消元回代法 122 3.8 数值计算 124 3.8.1 定积分计算 124 3.8.2 多项式求根(牛顿法) 125 3.8.3 周期性方程(追赶法) 127 4.排序 128 4.1快速选择算法 128 4.2归并排序+逆序数的求取 128 5.字符串 130 5.1 KMP应用 130 5.2 后缀数组 131 5.3 中缀表达式转后缀表达式 134 5.4 Firefighters 表达式求值 135 6.博弈 139 6.1 博弈的AB剪枝 139 6.1.1 取石子 139 6.2 博弈 SG函数 局势分割 141 7.数据结构 142 7.1 TRIE 142 7.2 线段树 147 7.3 并查集 151 7.4 树状数组 152 7.5 点树 154 7.6 STL 156 7.7 离散化 157 8.图论 158 8.0 2-SAT 158 8.2 寻找Euler回路 163 8.3 拓扑排序 163 8.4 差分约束系统 164 8.5 笛卡尔树 165 8.6 LCA和RMQ 167 8.7 割和桥 171 8.8 最小生成树(kruskal) 172 8.9 最短路径 173 8.10 最大网络流 175 8.11 最小费用流 180 8.12 最大团问题 182 8.13 二分图匹配 184 8.14 带权的最优二分图匹配 184 9.搜索算法概略 187 9.1 迭代深搜+IDA* 187 9.2 分之界限法(深搜) 189 9.3 A* 8数码问题( pascal ) 192 9.4 优先队列广搜 194 10.应用 197 10.1 Joseph问题 197 10.2 N皇后构造解 197 10.3 布尔母函数 198 10.4 第k元素 199 10.5 幻方构造 199 10.6 模式匹配(kmp) 201 10.7 逆序对数 201 10.8 字符串最小表示 202 10.9 最长公共单调子序列 202 10.10 最长子序列 204 10.11 最大子串匹配 204 10.12 最大子段和 205 10.13 最大子阵和 206 11.其它 207 11.1 大数(只能处理正数) 207 11.2 分数 212 11.3 矩阵 214 11.4 线性方程组 216 11. 5 线性相关 218 11.6 日期 219 11.7 读入 220 11.8 函数 220

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

memcpy0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值