工作中偶尔都会接触到一些二维数学相关的算法问题。
例如我在工作遇到的以下3个问题:
1. 我需要在地图上使用鼠标连线自定义一块闭合的区域出来。
具体的操作方法就是鼠标左键点击地图时,创建一个点、放开移动时根据上一个点以及鼠标的点形成连线,
最后点击右键闭包最开始的那个点,最终形成一个多边形,但是不能让线跟线出现交叉,否则最终形成的多边形就不对了。
2. 地图上有多线段图,我需要鼠标移动到线的附近时,切换鼠标光标状态。
3. 根据已有的两个垂直线段作为新的坐标系,确定一个点的坐标。
上述这3个问题都是一些常见的问题,其实大部分程序员都能够百度并最终解决这些问题,但是计算过程可能非常复杂。
其实如果使用高中向量的知识很容易解决这类问题。首先我们了解几个可能你已经忘了的向量的知识。
1. 向量点乘
A向量(x1,y1), B向量(x2,y2)。A·B = x1*x2 + y1*y2
代码如下(基于Qt的QPointF)
1 double pp(QPointF p1, QPointF p2) 2 { 3 return p1.x() * p2.x() + p1.y() * p2.y(); 4 }
点乘的特性:
1. 两个垂直的向量点乘为0。
2. 同一个向量点乘为向量的模的平方。
根据向量的点乘可以用来计算点到线段的距离。
具体的题型以及解法如下:(以下类似双字母AB这样且前面没有线段两字就表示为向量)
已知线段AB,其中A点为(a1,a2),B点为(b1,b2)。以及点C(c1,c2)。计算点C到线段AB的距离。
解: 假设D为点C在线段AB的投影点,那么CD垂直于AB。其中向量AD=k*AB。 k为实数
则 CD = CA + AD = CA + k * AB
CD * AB = 0 ==> (CA + k * AB) * AB = 0 ==> k = -(CA * AB)/(AB*AB) = (AC * AB)/(AB*AB)
继而就能算出CD向量,CD向量的模即是C到线段AB的距离。代码如下:
double pLen(QPointF p) { return sqrt(pp(p, p)); } /* 点到线的距离. 如果k值 在 0-1范围之内,证明点映射在直线的范围之内,否则在范围之外。 */ double PointToLine(QPointF p, QLineF l, double &k) { /* AC = (p-l.p0), AB = (l.p1 - l.p0),
AD = k * AB, 其中k为实数 DC = DA + AC = AC - AD = AC - k * AB AB ⊥ DC ==> AB * DC = 0 ==>
AB * (AC - k * AB) = 0
AB * AC - k * AB * AB = 0
k = (AB * AC) / (AB * AB) QPointF AC = p - l.p1(); QPointF AB = l.p2() - l.p1(); k = pp(AB, AC) / pp(AB, AB); QPointF DC = AC - k * AB; return pLen(DC); }
因为k值有时候是可以用到的,所以这里将k值也作为返回值,在解决问题2的时候,仅仅判断点到线段的距离小于某个值是不够。
还需确定点在线段的投影点在线段之内。当k值等于0时,投影点在A上,等于1时在B上。
2. 向量的叉乘
A向量(x1,y1), B向量(x2,y2)。A x B = x1*y2 - y1*x2
代码如下:
double xp(QPointF p1, QPointF p2) { return p1.x() * p2.y() - p1.y() * p2.x() }
叉乘有一个最重要的特性就是:
当两个向量共一个起点时所围成的三角形的面积是叉乘的0.5倍或者-0.5倍。
叉乘的正负取决两个向量叉乘时,哪个在前,哪个在后。
同样叉乘也可以用来计算点到线距离。
题型如下:
已知线段AB,其中A点为(a1,a2),B点为(b1,b2)。以及点C(c1,c2)。计算点C到线段AB的距离。
|AB x AC| = S(ABC) * 2 = |AB| * (C到AB距离)
C到AB的距离 = |AB X AC| / |AB|
//返回值有可能为负数,可确认点在有向线段的左边还是右边 double pointToLine2(QPointF p, QLineF l) { QPointF AB = l.p2() - l.p1(); QPointF AC = p - l.p1(); double C_AB = xp(AB, AC); return C_AB / pLen(AB) }
其中 AB X AC 的结果正负可以判定C在有向线段AB的左边还是右边。
另外问题1的核心在于判断线段与线段之间的关系,使用向量的叉乘很容易解决这个问题。
已知线段AB与线段CD。判断线段AB与线段CD的关系。
解:如果AB和线段CD相交,A点和B点一定在CD所在线的两边
同理,C点和D点在AB点的两端。
( CA x CD ) x (CB x CD) <= 0
( AB x AC ) x ( AB x AD) <= 0
满足这两个条件则 AB线段与 CD线段相交。
如果其中一个等于0, 则表示其中一个线段在另一个线段的之上
如果两个都等于0, 则表示AB与CD线段时收尾相连的。
int lineToLine(QLineF l1, QLineF l2) { QPointF AC = l2.p1() - l1.p1() QPointF AB = l1.p2() - l1.p1() QPointF AD = l2.p2() - l1.p1() double CD_AB = xp(AC, AB) * xp(AD, AB); if(CD_AB > 0) return 0; //不相交 QPointF CA = - AC; QPointF CB = l1.p2() - l2.p1() QPointF CD = l2.p2() - l2.p2(); double AB_CD = xp(CA, CD) * xp (CB, CD); if(AB_CD > 0) return 0; //不相交 if(AB_CD == 0 && CD_AB == 0) return 1; //首尾相连 return 2; //相交 }