首先线段可以用向量来表示,下面简单说一下:
1.矢量的概念:如果一条线段的端点是有次序之分的,我们把这种线段成为有向线段(directed segment)。如果有向线段p1p2的起点p1在坐标原点,我们可以把它称为矢量(vector)p2。
2.矢量加减法:设二维矢量P = ( x1, y1 ),Q = ( x2 , y2 ),则矢量加法定义为: P + Q = ( x1 + x2 , y1 + y2 ),同样的,矢量减法定义为: P - Q = ( x1 - x2 , y1 - y2 )。显然有性质 P + Q = Q + P,P - Q = - ( Q - P )。
再说说用向量来表示线段时的一个重要性质:叉积。
计算矢量叉积是与直线和线段相关算法的核心部分。设矢量P = ( x1, y1 ),Q = ( x2, y2 ),则矢量叉积定义为由(0,0)、p1、p2和p1+p2所组成的平行四边形的带符号的面积,即:P × Q = x1*y2 - x2*y1,其结果是一个标量。显然有性质 P × Q = - ( Q × P ) 和 P × ( - Q ) = - ( P × Q )。一般在不加说明的情况下,本文下述算法中所有的点都看作矢量,两点的加减法就是矢量相加减,而点的乘法则看作矢量叉积。叉积的一个非常重要性质是可以通过它的符号判断两矢量相互之间的顺逆时针关系:
若 P × Q > 0 , 则P在Q的顺时针方向。
若 P × Q < 0 , 则P在Q的逆时针方向。
若 P × Q = 0 , 则P与Q共线,但可能同向也可能反向。
Note :有人也许会问,为什么大于0就是顺时针方向呢? 实施上它是规定的,并且有其物理意义,可以看看中学物理里面,力、力矩、角速度、线速度的相关知识。
利用矢量叉乘的上述性质,可以用来判断折线段的拐向:
折线段的拐向判断方法可以直接由矢量叉积的性质推出。对于有公共端点的线段p0p1和p1p2,通过计算(p2 - p0) × (p1 - p0)的符号便可以确定折线段的拐向:
若(p2 - p0) × (p1 - p0) > 0,则p0p1在p1点拐向右侧后得到p1p2。
若(p2 - p0) × (p1 - p0) < 0,则p0p1在p1点拐向左侧后得到p1p2。
若(p2 - p0) × (p1 - p0) = 0,则p0、p1、p2三点共线。
这一条判断也可用来判断点在线段或直线的哪一测。
应用1: 确定两个线段是否相交?
#include <assert.h>
using namespace std;
const float MinimumFloatDeviation = 0.0001;
class Point
{
public:
float x, y;
Point(float _x, float _y)
{
x = _x;
y=_y;
}
bool operator==(const Point p1)
{
return (abs(x-p1.x)<MinimumFloatDeviation) && (abs(y - p1.y)<MinimumFloatDeviation);
}
bool iSInARectangle(Point p0, Point p1)
{
float minx,maxx,miny,maxy;
if(p0.x>p1.x)
{
minx = p1.x;
maxx = p0.x;
}else{
minx = p0.x;
maxx = p1.x;
}
if(p0.y>p1.y)
{
miny = p1.y;
maxy = p0.y;
}else{
miny = p0.y;
maxy = p1.y;
}
if(minx<=x && x<=maxx && miny<=y && y<=maxy) return true;
return false;
}
};
inline float direction(Point p_0, Point p_1, Point p_2)
{
Point p1(p_2.x - p_0.x, p_2.y - p_0.y);
Point p2(p_1.x - p_0.x, p_1.y - p_0.y);
return (p1.x*p2.y-p2.x*p1.y);
}
inline bool isSameDirection(float a)
{
return abs(a)<MinimumFloatDeviation;
}
bool isCrossed(Point p1, Point p2, Point p3, Point p4)
{
float d1 = direction(p3,p4,p1); // d1 means which direction p1 locates comparing to p3->p4 line, others are similar;
float d2 = direction(p3,p4,p2);
float d3 = direction(p1,p2,p3);
float d4 = direction(p1,p2,p4);
if(d1*d2 <0 && d3*d4<0) return true; // if p1 p2 are on different part pf p3->p4 line and p3 p4 are on different part of p1->p2 line, then the lines cross;
else if(isSameDirection(d1) && p1.iSInARectangle(p3,p4)) return true; // p1 is on p3->p4 line?
else if(isSameDirection(d2) && p2.iSInARectangle(p3,p4)) return true;
else if(isSameDirection(d3) && p3.iSInARectangle(p1,p2)) return true; // p3 is on p1->p2 line
else if(isSameDirection(d4) && p4.iSInARectangle(p1,p2)) return true;
return false;
}
int main(int argc, char **argv)
{
assert(isCrossed(Point(0,0), Point(2,3),Point(0,3), Point(2,6)) == false); // parallel
assert(isCrossed(Point(0,0), Point(2,3),Point(0,0), Point(2,3)) == true); // normal crosssed
assert(isCrossed(Point(0,0), Point(0,0),Point(0,0), Point(0,0)) == true); // same points
assert(isCrossed(Point(101.5,101.5), Point(101.5,101.5),Point(101.5,101.5), Point(101.5,101.5)) == true); // same points
assert(isCrossed(Point(0,0), Point(0,0),Point(0,0), Point(10,30.2)) == true); // one point is on another line;
assert(isCrossed(Point(-1,-1), Point(1,1),Point(-1,1), Point(1,-1)) == true); // another normal crossed;
assert(isCrossed(Point(0.1,100), Point(1000,0.01),Point(0.1001,100), Point(-1001,582)) == false); // another normal crossed;
assert(isCrossed(Point(0.1,100), Point(1000,0.01),Point(0.1,100), Point(-1001,582)) == true); // another normal crossed;
}
应用2: 在一组线段中,任意两个线段是否相交?
在这个问题的解答中,需要用到一种“扫除线”技术。 具体可以参考《算法导论(第二版)》33.2节,下面是简化后的分析和理解:
Note 1:补充一点自己的理解: 按照书中定义的方式对线段排序后,可以发现一个规律:如果两个线段相交,那么他们在某些扫描线上一定是连续的:如果新扫描到一条线段,也就是碰到了它的左端点,需要考虑与之连续的线段是否和它相交,由于它是新发现的线段,它改变了线段的连续状态,比如在它出现之前,a和b连续,但若c在ab之间,那连续状态就改变了,这时需要考虑和c连续的两个线段与c的关系;如果是要结束扫描一个线段,也就是碰到了它的右端点,它也会改变线段的连续状态,比如之前的连续状态是acb,如果要删除c,那a和b变成连续了,我们就要考虑a和b的相交关系了。
Note 2:该算法假定这一组线段中没有垂直的线段。如果有垂直线段,扫描线和这个垂直线段就有无数个交点,这样就无法对线段排序了,不过在代码中可以对这类直线做一些特殊处理,所以可以线排除垂直直线然后使用该算法;如果有三条线段相交于一点,