《程序员面试金典(第6版)》面试题 16.03. 交点(直线的一般式方程,克莱姆法则,行列式,C++)

news/2024/10/18 0:21:42/

题目描述

给定两条线段(表示为起点start = {X1, Y1}和终点end = {X2, Y2}),如果它们有交点,请计算其交点,没有交点则返回空值。

  • 要求浮点型误差不超过10^-6。若有多个交点(线段重叠)则返回 X 值最小的点,X 坐标相同则返回 Y 值最小的点。

示例 1:

  • 输入:
    line1 = {0, 0}, {1, 0}
    line2 = {1, 1}, {0, -1}
  • 输出: {0.5, 0}

示例 2:

  • 输入:
    line1 = {0, 0}, {3, 3}
    line2 = {1, 1}, {2, 2}
  • 输出: {1, 1}

示例 3:

  • 输入:
    line1 = {0, 0}, {1, 1}
    line2 = {1, 0}, {2, 1}
    -输出: {},两条线段没有交点

提示:

  • 坐标绝对值不会超过 2^7
  • 输入的坐标均是有效的二维坐标

解题思路与代码

这道题,乍一看是不是很简单,方法就是像初高中的一道数学题。让你去判断两条直线是否具有交点

但是呢,这道题是让你去判断两条线段是否具有交点。并且,如果有多个交点,返回最小的那一个。(在多个交点中,有且只有一个交点,它的x的值最小,那么返回这个x最小的交点。如果交点的x值都相同,则返回y值最小的交点)

不要小看线段与直线的区别,就这一点区别,让整道题的难度激增。

并且,关于直线的方程式有好多种,比如一般式方程啦,点斜式方程啦,斜截式方程啦,截距式方程啦,参数式方程啦,选择用哪一个,这也是重中之重。因为这些方程式各有优缺点。选错了,你解题的步骤又可能激增,然后等于是难上加难再加难。对就是这样。

我这边,特地的选择了一种,最好理解的方式来解决这道题。也就是使用一般式方程进行求解。这种方式最大的好处就是,写出来的代码简单易懂。

并且,这道题实际上是很偏数学的,数学不好,或者说数学与代码的结合能力差,也是根本不可能做的出来的。

如果不是面试游戏公司,应该不会出这样的代数题来为难大家吧,hhhhh。

废话不多说了,开始带大家解题吧。

方法一 : 直线的一般式方程求解

首先做这道题,需要你有一定的数学储备知识。不过你没有也没关系,就听我讲就完事了。

基础知识

首先我们解决这道题采用的是直线的一般式方程,也就是说形如Ax + By + C = 0;用到了这个方程的两个结论。

  • 一个结论就是,如果两条直线平行,则 A1B2 = A2B1

  • 其次就是,任何直线Ax + By + C = 0 中的A,B,C,都可以用两个点的横轴坐标这么去表示:

    • A = y2 - y1;
    • B = x1 - x2;
    • C = x2y1 - x1y2;
  • 如果你对这两个结论感到疑惑了话,可以去看这个文章,去补充自己的基础知识: 直线的一般式方程

还有一个知识就是关于克莱姆法则,这个涉及到大学的线性代数中的行列式的一点基础知识。也不难。如果你想要具体了解什么是克莱姆法则的话,可以去看这篇文章去补充相关的基础知识:克莱姆法则

  • 如果大家懒的看这个百度百科的克莱姆法则,你也可以看看我说的简化版的。

  • 克莱姆法则(Cramer’s Rule)是一种用于求解线性方程组的方法,适用于具有唯一解的线性方程组。克莱姆法则使用行列式(determinants)来求解方程组,它的基本思想是将原线性方程组的系数矩阵通过替换列的方法,变换成相应的行列式,然后通过计算这些行列式的值来求解未知数。

  • 对于一个 n 阶线性方程组,如果系数行列式不为零(即方程组具有唯一解),那么可以使用克莱姆法则求解。具体求解方法是将系数矩阵的第 i 列替换为方程组的右侧常数项构成的列向量,然后计算新的行列式,再将这个行列式与原系数行列式之比作为未知数 x_i 的解。

  • 那么对于这道题来说,我们有4个点,也就是两条线段。我们可以把它先理解为两条直线,那么就会有两条一般式的直线方程。也就组成了一个二元的方程组。

    • A1x + B1y + C1 = 0; => A1x + B1y = -C1;(方程组右侧是-C1)
    • A2x + B2y + C2 = 0; => A2x + B2y = -C2;(方程组右侧是-C2)
  • 在每一个方程里只有两个未知数,所以这个二元一次方程组的系数矩阵(记作:D)为:

    |A1 B1|
    |A2 B2|
    
  • 计算行列式的方法是主对角线(从左上到右下)的乘积 - 副对角线(从左下到右上)的乘积。所以D这个系数矩阵的值为 :A1B2 - A2B1;

  • 我们将第一列替换为常数项列向量,得到矩阵:

| -c1  b1 |
| -c2  b2 |
  • 设 Dx 为这个矩阵的行列式值,即 Dx = -c1 * b2 - (-c2 * b1)。那么解 x1 = Dx / D。

  • 同理,我们将第二列替换为常数项列向量,得到矩阵:

    | a1  -c1 |
    | a2  -c2 |
    
  • 设 Dy 为这个矩阵的行列式值,即 Dy = a1 * c2 - a2 * c1。那么解 x2 = Dy / D。

  • 我们可以把这组解x1,x2看做是两条直线的交点的横纵坐标,x1 = x, x2 = y,

    • 所以 x = Dx/D
    • 所以 y = Dy/D

好,知道这么多数学知识,就够我们去解决这道代码题的了。还是强调一下,直线的一般式方程求解是众多直线方程中最好理解的一种了。但就这一种也够复杂的了,如果不是面游戏公司,注重几何的,没必要再深究这道题的其他解法。

解题步骤

对于这道题,我们需要写这么几个函数,分别是:

  • 计算方程参数ABC的函数
  • 寻找当线段平行时是否存在符合题目的交点的函数
  • 判断交点是否在两条线段上的函数
  • 然后就是用主函数集合这么几个函数来求得最终的解。
    • 我们在主函数里先这么去写逻辑,首先判断俩直线是否平行,如果平行则,调用寻找当线段平行时是否存在符合题目的交点的函数,去找符合的点,找不到则返回空
    • 其次用计算方程参数ABC的函数去计算两天线段的一般式方程的ABC参数分别是多少,分别存储在两个double的vector中。
    • 克莱姆法则求出可能交点,再用判断交点是否在两条线段上的函数判断交点是否在两条线段上,若在,返回交点,否则返回空
    • 至此本题解完。

具体实现请看代码:

class Solution {
public:vector<double> intersection(vector<int>& start1, vector<int>& end1, vector<int>& start2, vector<int>& end2) {//由一般式方程Ax + By + C = 0 的结论可知 A = y2 - y1; B = x1 - x2; C = x2y1 - x1y2;int A1 = end1[1] - start1[1];int B1 = start1[0] - end1[0];int A2 = end2[1] - start2[1];int B2 = start2[0] - end2[0];//如果线段平行,则去寻找是否有这样一个符合条件的点,找到则返回交点,否则返回空if(A1 * B2 == A2 * B1) return findPointOnTheParallelLine(start1,end1,start2,end2);//获取两条直线方程的参数ABCvector<double> p1 = getParm(start1,end1);vector<double> p2 = getParm(start2,end2);//根据克莱姆法则计算可能交点(x,y)的坐标;double x = (p2[2] * p1[1] - p1[2] * p2[1]) / (p1[0] * p2[1] - p2[0] * p1[1]);double y = (p1[2] * p2[0] - p2[2] * p1[0]) / (p1[0] * p2[1] - p2[0] * p1[1]);vector<double> result{x,y};return (isIntersectionPoint(result,start1,end1) && isIntersectionPoint(result,start2,end2)) ? result : vector<double>{};}vector<double>findPointOnTheParallelLine(vector<int>& start1, vector<int>& end1, vector<int>& start2, vector<int>& end2){vector<vector<double>> result;if(isIntersectionPoint(start1,start2,end2)) result.push_back(vector<double>{double(start1[0]), double(start1[1])});if(isIntersectionPoint(start2,start1,end1)) result.push_back(vector<double>{double(start2[0]), double(start2[1])});if(isIntersectionPoint(end1,start2,end2)) result.push_back(vector<double>{double(end1[0]), double(end1[1])});if(isIntersectionPoint(end2,start1,end1)) result.push_back(vector<double>{double(end2[0]), double(end2[1])});if(result.empty()) return vector<double>{};sort(result.begin(),result.end(),[](vector<double>& l, vector<double>& q) -> bool{return l[0] < q[0];});return result[0];}vector<double> getParm(vector<int>& s, vector<int>& e){return {double(e[1] - s[1]),double(s[0] - e[0]),double(e[0] * s[1] - s[0] * e[1])};}template<typename T1, typename T2, typename T3>bool isIntersectionPoint(T1 &p, T2 &s, T3 &e){double flag = 1e-7;double d1 = sqrt((p[0] - s[0]) * (p[0] - s[0]) + (p[1] - s[1]) * (p[1] - s[1]));double d2 = sqrt((p[0] - e[0]) * (p[0] - e[0]) + (p[1] - e[1]) * (p[1] - e[1]));double d3 = sqrt((s[0] - e[0]) * (s[0] - e[0]) + (s[1] - e[1]) * (s[1] - e[1]));return fabs(d1 + d2 - d3) <= flag;}
};

在这里插入图片描述

复杂度分析

这段代码实现了一个计算两条线段交点的算法。以下是对主要函数的时间复杂度与空间复杂度的分析:

intersection() 函数:

  • 时间复杂度:O(1),因为这个函数中的所有操作都是常数时间操作,没有循环或递归。
  • 空间复杂度:O(1),函数内部创建了常数个变量和容器,没有使用动态分配内存。

findPointOnTheParallelLine() 函数:

  • 时间复杂度:O(1),这个函数中的所有操作都是常数时间操作,没有循环或递归。
  • 空间复杂度:O(1),函数内部创建了常数个变量和容器,没有使用动态分配内存。

getParm() 函数:

  • 时间复杂度:O(1),这个函数中的所有操作都是常数时间操作,没有循环或递归。
  • 空间复杂度:O(1),函数内部创建了常数个变量和容器,没有使用动态分配内存。

isIntersectionPoint() 函数:

  • 时间复杂度:O(1),这个函数中的所有操作都是常数时间操作,没有循环或递归。
  • 空间复杂度:O(1),函数内部创建了常数个变量和容器,没有使用动态分配内存。

整个算法的时间复杂度和空间复杂度都是 O(1),因为所有函数都具有常数时间和空间复杂度。

总结

这道题确实是一道困难题,但也是一道十分有乐趣的题。因为它很巧妙的把数学的几何知识融入到了算法题中,让我见识到了数学与算法的结合。

  • 最后的最后,如果你觉得我的这篇文章写的不错的话,请给我一个赞与收藏,关注我,我会继续给大家带来更多更优质的干货内容

http://www.ppmy.cn/news/51133.html

相关文章

服务(第十二篇)LVS-DR模式

数据包流向分析&#xff1a; &#xff08;1&#xff09;客户端发送请求到 Director Server&#xff08;负载均衡器&#xff09;&#xff0c;请求的数据报文&#xff08;源 IP 是 CIP,目标 IP 是 VIP&#xff09;到达内核空间。 &#xff08;2&#xff09;Director Server 和 Re…

PCL点云库(3) — common模块

目录 3.1 common模块中的头文件 3.2 common模块中的基本函数 &#xff08;1&#xff09;angle角度转换 &#xff08;2&#xff09;distance距离计算 &#xff08;3&#xff09;random随机数生成 &#xff08;4&#xff09;sping扩展模块 &#xff08;5&#xff09;time获…

同步和异步的区别及优缺点 通俗理解

一、同步和异步的区别 程序里面的同步和异步和我们现实生活理解不太一样&#xff0c;一般我们对同步的理解是同时做很多事情&#xff0c;但程序中的同步是按照任务的顺序执行任务&#xff0c;前一个任务没有执行结束&#xff0c;下一个任务不会执行&#xff0c;要等待上一个任…

一个注解解决分布式锁和接口幂等性,springboot 实战 。强到离大谱

如今基本上都是分布式、多节点时代&#xff0c;我们业务代码中避免不了需要使用分布式锁。admin4j-lock 为我们提供分布式锁解决方案。支持redisson和zookeeper分布式锁 功能 支持redisson分布式锁和zookeeper 分布式锁 支持可重入锁 支持读写锁 支持红锁 redLock 支持一个…

Python 程序通过可执行文件部署

以下是两种常用的打包 Python 程序成 exe 的方式&#xff1a; PyInstaller&#xff1a; PyInstaller 是一个用于将 Python 程序打包成独立的可执行文件的工具。它可以自动解决 Python 程序的依赖性&#xff0c;并将所有必要的文件&#xff08;包括 Python 解释器&#xff09;…

2023年五月份图形化四级打卡试题

活动时间 从2023年5月1日至5月21日&#xff0c;每天一道编程题。 本次打卡的规则如下&#xff1a; 小朋友每天利用10~15分钟做一道编程题&#xff0c;遇到问题就来群内讨论&#xff0c;我来给大家答疑。 小朋友做完题目后&#xff0c;截图到朋友圈打卡并把打卡的截图发到活动群…

如何写出高质量代码——站在巨人的肩膀上

如何写出高质量代码——站在巨人的肩膀上 高质量代码的三要素&#xff1a;可读性&#xff0c;可维护性&#xff0c;可变更性可读性强可维护性&#xff1a;适应软件在部署和使用中的各种情况1.3 可变更性&#xff1a;因需求变化而对代码进行修改 牛顿曾经说过&#xff1a;如果说…

机器思维(个人总结)

机器思维&#xff0c;也称为人工智能或AI&#xff0c;是一种由计算机程序或机器实现的智能行为和决策的领域。这种智能可以表现为对自然语言的理解和生成、对图像和声音的理解、对环境的感知和理解、对复杂问题的推理和决策等&#xff0c;这些都是人类智能的核心特征。人工智能…