(数学人狂喜!)三点共线问题:回旋镖问题算法们的讨论 | 豆包MarsCode AI刷题

131 阅读7分钟

回旋镖算法分享

判断回旋镖的存在问题是题库里的一道简单题,它的实现实则是考察我们对数学的掌握程度。而它的实现其实有很多种方式,下面就来介绍其中的4种。

  • 一次函数法
  • 距离法
  • 向量法
  • 行列式法

0.题目复现

问题描述

小M正在玩一个几何游戏,给定一个二维平面上的三个点 points,其中每个点用坐标 points[i] = [xi, yi] 表示。如果三点构成一个回旋镖,则返回 true。回旋镖的定义是三点不在一条直线上,并且这三个点互不相同。

请你帮助小M判断这些点是否构成一个回旋镖。

测试样例

样例1:

输入:points = [[1, 1], [2, 3], [3, 2]]
输出:True

样例2:

输入:points = [[1, 1], [2, 2], [3, 3]]
输出:False

样例3:

输入:points = [[0, 0], [1, 1], [1, 0]]
输出:True

实现这个问题的思路很简单:就是使用数学知识去证明任意三点是否共线,若共线返回false,若不共线则返回True。

下面笔者基于Python去写这个问题的算法实现。

法1:一次函数法

由我们初中就学过的数学知识可知,任意两点必定可以构成一条直线,也就是说,任意两点必定可以构成一个一次函数。如果三个点共线,那么它们必定在同一个函数直线上。如果三点不共线,那么其中两点在同一个函数直线上,但第三个点不在这条函数上。

因此,在这个问题里:

  • Step 1:我们可以在给定的样例里,任意选取2个点构成一条直线,求取这个函数的参数。(斜率m和截距b
  • Step 2:然后再用第三个点带入这个函数,若第三个点不在这个函数上,则三点不共线,可以构成回旋镖,返回True。若第三个点在这个函数上,则三点共线,构不成回旋镖,返回False。

法1的算法实现

def solution(points: list) -> bool:
    # write code here
    # 函数法实现
    x1,y1=points[0]
    x2,y2=points[1]
    x3,y3=points[2]
    
    #任意提取两点出来构建一次函数
    if(x2!=x1): #排除特殊情况,当x1=x2时,斜率不存在,此时函数为y=b
        m=(y2-y1)/(x2-x1)#计算斜率
        b=y1-m*x1 #计算截距
        if (y3!=x3*m+b):
            return True
        else:
            return False
    else:
        if(x3!=x1):
            return True
        else:
            return False
   
if __name__ == '__main__':
    print(solution(points=[[1, 1], [2, 3], [3, 2]]) == True) #True
    print(solution(points=[[1, 1], [2, 2], [3, 3]]) == False) #True
    print(solution(points=[[0, 0], [1, 1], [1, 0]]) == True) #True

这个算法不仅只需要很简单的数学知识,而且用编程语言实现也非常简单,对新手友好。但是笔者认为它有一个麻烦的地方就是需要考虑斜率不存在的时候。

法2:距离法实现

顾名思义,这个算法的实现逻辑就是:计算两点之间的距离来判断三点是否共线。如果任意两段距离之和等于第三段距离,则三点共线;否则,三点不共线。

这个方法实现起来也很简单,而且不用考虑特殊情况。它的核心就是要使用欧几里得距离公式去计算两点距离。

欧几里得距离公式: image.png

算法步骤:

  • Step 1:计算任意两点间的距离。
  • Step 2:判断任意两点距离之和是否等于第三段距离。

法2的算法实现

import math

def solution(points: list) -> bool:
    # 检查三点是否互不相同
    if points[0] == points[1] or points[1] == points[2] or points[0] == points[2]:
        return False
    
    # 提取坐标
    x1, y1 = points[0]
    x2, y2 = points[1]
    x3, y3 = points[2]
    
    # 计算两点之间的距离
    d1 = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
    d2 = math.sqrt((x3 - x2) ** 2 + (y3 - y2) ** 2)
    d3 = math.sqrt((x3 - x1) ** 2 + (y3 - y1) ** 2)
    
    # 判断三点是否共线
    if abs(d1 + d2 - d3) < 1e-9 or abs(d1 + d3 - d2) < 1e-9 or abs(d2 + d3 - d1) < 1e-9:
        return False
    else:
        return True

if __name__ == '__main__':
    print(solution(points=[[1, 1], [2, 3], [3, 2]]) == True)
    print(solution(points=[[1, 1], [2, 2], [3, 3]]) == False)
    print(solution(points=[[0, 0], [1, 1], [1, 0]]) == True)

值得注意的是:在Python中,浮点数的比较可能会出现精度误差,因此我们在判断机制里,使用了一个很小的误差范围1e-9来判断两个浮点数是否相等。

法3:向量法实现

刚刚的距离法实际是把点的坐标转换为了向量,然后再去计算向量的长度。显然,如果我们直接通过计算向量来判断三点共线问题,会让我们的问题解决得更有效率。

因此,在这个算法里,我们可以使用向量的叉乘来判断三点是否共线。如果三个点共线,则它们的向量叉乘结果为0。

而在Python里,我们可以使用NumPy库来实现向量的叉乘,也可以直接使用Python内置的元组数据类型来存储数据。

算法步骤:

  • Step 1:提取三个点的坐标,构建任意两个点组成的向量,并使用二维元组来存储向量v1 = (x2 - x1, y2 - y1)
  • Step 2:计算两个向量的叉乘。
  • Step 3:判断叉乘结果是否为0。

法3的算法实现

def solution(points: list) -> bool:
    # 检查三点是否互不相同
    if points[0] == points[1] or points[1] == points[2] or points[0] == points[2]:
        return False
    
    # 提取坐标
    x1, y1 = points[0]
    x2, y2 = points[1]
    x3, y3 = points[2]
    
    # 计算向量 v1 和 v2
    v1 = (x2 - x1, y2 - y1)
    v2 = (x3 - x1, y3 - y1)
    
    # 计算向量 v1 和 v2 的叉积
    cross_product = v1[0] * v2[1] - v1[1] * v2[0]
    
    # 如果叉积为零,则三点共线
    if cross_product == 0:
        return False
    else:
        return True

if __name__ == '__main__':
    print(solution(points=[[1, 1], [2, 3], [3, 2]]) == True)
    print(solution(points=[[1, 1], [2, 2], [3, 3]]) == False)
    print(solution(points=[[0, 0], [1, 1], [1, 0]]) == True)

显然,在这个算法里,我们使用了元组来存储向量,而元组是不可变的,因此它的计算速度将大大提高。

法4:行列式法实现

这个方法的实现思路和向量法是一样的,都是通过计算向量的叉乘来判断三点是否共线。通过对高等代数等相关课程的学习,我们可以知道:如果行列式的值为零,则三点共线;否则,三点不共线。

  • Step 1:提取三个点的坐标。
  • Step 2:计算行列式.
  • Step 3:判断行列式的值是否为零。

法4的算法实现

def solution(points: list) -> bool:
    # 检查三点是否互不相同
    if points[0] == points[1] or points[1] == points[2] or points[0] == points[2]:
        return False
    
    # 提取坐标
    x1, y1 = points[0]
    x2, y2 = points[1]
    x3, y3 = points[2]
    
    # 计算行列式
    determinant = x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)
    
    # 如果行列式为零,则三点共线
    if determinant == 0:
        return False
    else:
        return True

if __name__ == '__main__':
    print(solution(points=[[1, 1], [2, 3], [3, 2]]) == True)
    print(solution(points=[[1, 1], [2, 2], [3, 3]]) == False)
    print(solution(points=[[0, 0], [1, 1], [1, 0]]) == True)

5.总结与延伸

在这个问题里,我们使用了4种不同的算法来实现回旋镖的判断。其中,向量法和行列式法的实现是最为高效的,它们的时间复杂度都为O(1)。而距离法和一次函数法的实现则相对较为简单,它们的时间复杂度都为O(1)。

另外,笔者在这个问题里还使用了一些Python的技巧,例如元组的使用、浮点数的比较等。这些技巧可以帮助我们更加高效地实现算法。

以及,其实除了上述4种方法,笔者还想到一些别的算法思路去实现该问题:

比如说,我们是否可从图论的角度去构建算法?笔者从七桥问题受到启发,如果三点不共线,那么可视为它们在一个图里,它们必然构成一个环。反之则不能。于是乎,只要我们通过分析图的连通性,就能判断三点是否构成一个回旋镖。

但由于时间关系,并没有将它们用计算机语言实现。但有空的话我觉得还是很值得敲一敲的。