让我们从一个很简单的问题开始:如何判断线段(A1, A2) (B1, B2)是否相交(假设A2>A1, B2>B1)?
对于以上问题,如果我给出如下代码:
IsJoin := ABS(A1+A2-B1-B2) <= (A2-A1+B2-B1);
此时,你一定对这行代码的有效性充满了疑惑。没关系,下面让我们从普通解法开始,慢慢推导出上面的结论。
根据分析,两条线段的关系可能有如下情况:
1: A1-------A2 B1---------B2
2: B1---------B2 A1-------A2
3: A1----B1---A2--------B2
4: B1----A1----B2-----A2
5: A1---B1---------B2----A2
6: B1------A1-------A2---B2
可见,只要A1,A2,B1,B2四个点,只要有一个点落在另一条线段范围内,两条线段就相交。
代码如下:
IsJoin := A1>=B1&&A1<=B2 || A2>=B1&&A2<=B2 || B1>=A1&&B1<=A2 || B2>=A1&&B2<=A2;
这是我们马上就能想到的解法,相信不会有人怀疑他的有效性。
但是这段代码最大的问题是重复度和相似度太高,很容易写错却很难发现。
下面,我们通过数学方法,给出这个问题的数学解。
从几何意义上,判断条线段相交,可以通过判断两条线段中心点的距离小于线段长度和的一半来实现。
而两个点X,Y的距离,可以用两点差的绝对值表示:ABS(X-Y)
则两条线段中心点距离为:ABS((A1+A2)/2 - (B1+B2)/2)
两条线段长度和一半可以表示为 (A2-A1)/2 + (B2-B1)/2
因此,判断两条线段是否相交,可以表示为 ABS((A1+A2)/2-(B1+B2)/2) <= (A2-A1)/2+(B2-B1)/2
两边同时乘以2并化简即得:ABS(A1+A2-B1-B2) <= (A2-A1+B2-B1)
进一步,如果我们把问题更深入一步,描述为:
如何判断线段(A1,A2)和(B1,B2)谁跟线段(C1,C2)相交更深。
如以下情况,(B1,B2)与(C1,C2)相交更多。
A1------C1-A2--------B1-----C2------B2
相信此时,普通解法只能说抱歉打扰了,臣妾做不到哇。
但此时,数学解法还可以进一步优化,如何判断两条线段的距离?
实际上,通过判断两条线段中心点的距离跟线段长度和一半的比值,既可以判断两条线段是否相交,更能评价两条线段相交有多“深”。
DistanceAC := ABS(A1+A2-C1-C2)/(A2-A1+C2-C1);
DistanceBC := ABS(B1+B2-C1-C2)/(B2-B1+C2-C1);
此时,判断线段A,C是否相交,只需要改为判断 DistanceAC<=1 即可。
相应的,判断AC和BC谁相交更深,只需要判断 DistanceAC<=DistanceBC 即可。
通过以上解法分析,可以看到,程序的灵魂其实是数学原理。
不要以为花三天时间学会了一门编程语言,会写几行代码就是合格程序员了。
在计算机领域,成就最大的人,都不一定会写代码,但他一定是优秀的数学家或擅长用数学思考的人。
程序之美在于数学之美,隐藏在悠美代码背后的数学原理,才是程序的灵魂。