程序员数学:用Python学透线性代数和微积分
上QQ阅读APP看书,第一时间看更新

3.2 三维空间中的向量运算

有了这些Python函数,在三个维度上对向量运算的结果进行可视化就变得很简单了。二维平面上的所有算术运算在三维空间中都有对应的运算,而且其几何效果是类似的。

3.2.1 添加三维向量

在三维空间中,向量加法仍可以通过将坐标相加来完成。向量(2, 1, 1)和(1, 2, 2)相加为(2+1, 1+2, 1+2) = (3, 3, 3)。从原点开始,将两个输入向量首尾相接,就可以得到求和之后的点(3, 3, 3)(见图3-15)。

图3-15 三维向量加法的两种可视化示例

与在二维平面上一样,要想把任意数量的三维向量相加,可以将它们的所有坐标、所有坐标和所有坐标分别相加。有了这三个和,就能得到新向量的坐标。例如,对(1, 1, 3)、(2, 4, -4)和(4, 2, -2)求和。因为它们各自的坐标是1、2、4,相加为7,坐标的和也是7,而坐标的和是 -3,所以向量的和是(7, 7, -3)。这三个向量首尾相接看起来如图3-16所示。

图3-16 在三维空间中添加三个首尾相接的向量

在Python中,可以编写一个简洁的函数来对任意数量的输入向量求和,并在二维或三维(或之后更高的维度)空间中使用,如下所示。

def add(*vectors):
    by_coordinate = zip(*vectors)
    coordinate_sums = [sum(coords) for coords in by_coordinate]
    return tuple(coordinate_sums)

下面来分析一下。在输入向量上调用Python的zip函数,可以提取它们的坐标、坐标和坐标。例如:

>>> list(zip(*[(1,1,3),(2,4,-4),(4,2,-2)]))
[(1, 2, 4), (1, 4, 2), (3, -4, -2)]

(需要将zip结果转换为列表来显示它的值。)如果将Python的sum函数应用到每个分组坐标上,将会获得值的和,分别为:

[sum(coords) for coords in [(1, 2, 4), (1, 4, 2), (3, -4, -2)]]
[7, 7, -3]

最后,为了保持一致,需要将这个列表转换为元组,因为到目前为止,所有的向量都以元组的形式表示。结果就是元组(7, 7, 3)。add函数也可以写成下面的单行代码(这可能不那么有Python风格)。

def add(*vectors):
    return tuple(map(sum,zip(*vectors)))

3.2.2 三维空间中的标量乘法

将三维向量乘以标量,就是把其所有分量乘以标量系数。例如,向量(1, 2, 3)乘以标量2,会得到(2, 4, 6)。由此产生的向量长度是二维情况下的两倍,但两者指向相同的方向。图3-17显示了和它的标量乘积

图3-17 乘以标量2之后返回指向同一方向的向量,该向量的长度是原向量的2倍

3.2.3 三维向量减法

在二维平面上,两个向量的差值就是“从”的向量,称为位移。在三维空间中也是一样的,换句话说,就是从的位移,把这个向量与相加即可得到。将看作从原点出发的箭头,那么的差值也是一个箭头,它的头部位于的头部,尾部位于的头部。图3-18显示了的差值,它既是从的箭头,本身也是一个点。

图3-18 从向量中减去向量,得到从的位移

从向量中减去向量,在坐标上是通过取的坐标之差来完成的。例如,的结果是(-1 - 3, -3 - 2, 3 - 4) = (-4, -5, -1),这些坐标与图3-18中的一致,表明它是一个指向负、负和负方向的向量。

在说乘以标量2让向量变成“2倍长”时,我是出于对几何相似性的考虑。如果将向量的3个分量分别翻倍,相当于框的长度、宽度和深度都翻倍,那么从一个角到其对角的距离也应该翻倍。为了实际测量和确认这一点,需要知道如何计算三维空间中的距离。

3.2.4 计算长度和距离

在二维平面上,我们通过勾股定理来计算向量的长度,因为箭头向量和它的分量构成了一个直角三角形。同样,平面内两点之间的距离也只是它们作为向量的差的长度。

计算三维空间中向量的长度需要更仔细地观察,不过仍然存在合适的直角三角形作为辅助。首先试着算出向量(4, 3, 12)的长度。分量和分量仍然构成了一个直角三角形的两条边,位于的平面中。这个三角形的斜边(对角线)长度为。如果这是二维向量,就已经计算完成了,但长度为12的分量拉长了这个向量(见图3-19)。

图3-19 应用勾股定理求平面内的斜边长度

到目前为止,我们研究的所有向量都位于平面内,其中分量是(4, 0, 0),分量是(0, 3, 0),它们的向量和是(4, 3, 0)。分量(0, 0, 12)垂直于这三个向量。这很有用,因为有了它就有了图中的第二个直角三角形:由(4, 3, 0)和(0, 0, 12)两个向量首尾相连构成的三角形。这个三角形的斜边就是开始时想要计算长度的向量 (4, 3, 12)。接下来看第二个直角三角形,并再次使用勾股定理来算出斜边的长度(如图3-20所示)。

图3-20 再次使用勾股定理,算出三维向量的长度

计算两条已知边的平方,然后取平方根,就可以得到长度。这里两条边的长度是5和12,所以结果是。总而言之,下面是三维向量的长度公式。

长度

它恰好和二维长度公式很相似。无论对于二维还是三维,向量的长度都是其分量平方和的平方根。因为下面的length函数并没有用到输入元组的长度,所以它对二维和三维向量都适用。

from math import sqrt
     def length(v):
         return sqrt(sum([coord ** 2 for coord in v]))

例如,length((3,4,12))返回13。

3.2.5 计算角度和方向

像二维向量一样,三维向量可以被看作箭头或者沿一定方向发生的一定长度的位移。在二维平面上,这意味着两个数(一个长度和一个角度,构成一对极坐标)足以指定任何二维向量。在三维空间中,一个角度不足以确定方向,但两个可以。

对于第一个角度,可以再次考虑没有坐标的向量,就好像它仍然在平面上一样。另一种思考方式是,该角度是由来自非常高的位置的光投射在向量上形成的阴影。这个阴影与轴正方向形成一定的角度,类似于极坐标中的角度,并使用希腊字母来表示。第二个角度是向量与轴正方向的夹角,用希腊字母来表示。图3-21显示了这些角度。

图3-21 两个角度共同指定三维向量的方向

向量的长度用来表示,它与角度一起可以描述三维空间中的任何向量。这三个数组成了球坐标系,与笛卡儿坐标系完全不同。仅使用之前讲过的三角学知识,根据笛卡儿坐标系计算球坐标是可行的,但这里不会深入讨论。事实上,本书不会再使用球坐标了,但我想简单地将其与极坐标比较一下。

对于极坐标,可以通过简单的角度加减来执行平面向量集合的任意旋转。对于极坐标,也能通过对两个向量的角度取差,来获取它们的夹角。在三维空间中,单凭角都不能立即确定两个向量之间的角度。虽然通过加减角可以轻松地绕轴旋转向量,但在球坐标系中绕任何其他的轴旋转都不方便。

我们需要一些更通用的工具来处理三维空间中的角度和三角学。下一节会介绍两种这样的工具,称为向量积

3.2.6 练习

练习3.3:将(4, 0, 3)和(-1, 0, 1)绘制为Arrow3D对象,使它们在三维空间中以两种顺序首尾相接。它们的向量和是多少?

:可以用我们的add函数找到向量和。

>>> add((4,0,3),(-1,0,1))
(3, 0, 4)

为了绘制首尾相接的箭头,首先画出从原点到每个点的箭头,再画出从每个点到向量和(3, 0, 4)的箭头(见图3-22)。和二维的Arrow对象一样,Arrow3D也先取箭头的头部向量,然后可选地取尾部向量(如果它不是原点)。

draw3d(
    Arrow3D((4,0,3),color=red),
    Arrow3D((-1,0,1),color=blue),
    Arrow3D((3,0,4),(4,0,3),color=blue),
    Arrow3D((-1,0,1),(3,0,4),color=red),
    Arrow3D((3,0,4),color=purple)
)

图3-22 首尾加法显示,(4, 0, 3) + (-1, 0, 1) = (-1, 0, 1) + (4, 0, 3) = (3, 0, 4)

 

练习3.4:假设设置vectors1=[(1,2,3,4,5),(6,7,8,9,10)]vectors2=[(1,2),(3,4),(5,6)]。在不使用Python求值的情况下,zip(*vectors1)zip(*vectors2)的长度分别是多少?

:第一个zip的长度为5。因为两个输入向量中各有5个坐标,所以zip(vectors1)包含5个元组,每个元组有两个元素。同样,zip(vectors2)的长度为2。zip(vectors2)的两个条目分别是包含所有分量和所有分量的元组。

 

练习3.5(小项目):下面的代码创建了一个包含24个Python向量的列表。

from math import sin, cos, pi
vs = [(sin(pi*t/6), cos(pi*t/6), 1.0/3) for t in range(0,24)]

这24个向量的和是多少?把这24个向量绘制成首尾相接的Arrow3D对象。

:首尾相接地依次绘制这些向量,最终会形成螺旋状(见图3-23)。

from math import sin, cos, pi
vs = [(sin(pi*t/6), cos(pi*t/6), 1.0/3) for t in range(0,24)]

running_sum = (0,0,0)      ←---- 在(0, 0, 0)处初始化动态和,从这里开始从头到尾相加
arrows = []
for v  in vs:
    next_sum = add(running_sum, v)      ←---- 绘制后续首尾相接的向量时,把它加到动态和上。最新的箭头把前一个动态和与下一个连接起来
    arrows.append(Arrow3D(next_sum, running_sum))
    running_sum = next_sum
print(running_sum)
draw3d(*arrows)

图3-23 求三维空间中24个向量的和

得到的和为:

(-4.440892098500626e-16, -7.771561172376096e-16, 7.9999999999999964)

大约是(0, 0, 8)。

 

练习3.6:编写函数scale(scalar,vector),返回输入标量乘以输入向量的结果。具体地说,这个函数要同时适用于二维和三维向量,以及有任意多坐标的向量。

:通过推导运算,将向量中的每个坐标乘以标量。这是一个被转换成元组的生成器推导式。

def scale(scalar,v):
    return tuple(scalar * coord for coord in v)

 

练习3.7:设的结果是什么?

:已知,首先计算。那么就是(-1/2, 1/2, 3/2)。最终得到结果。顺便说一下,这正好是点和点的中点。

 

练习3.8:试着在不使用代码的情况下找到这个练习的答案,然后检查你的答案是否正确。二维向量(1, 1)的长度是多少?三维向量(1, 1, 1)的长度是多少?我们还没有讨论到四维向量,但是它们有四个坐标,而不是两个或三个。猜一下,坐标为(1, 1, 1, 1)的四维向量的长度是多少?

:(1, 1)的长度为。(1, 1, 1)的长度是。正如你可能猜到的,同样的距离公式对高维向量也适用。(1, 1, 1, 1)的长度遵循同样的规律:它的长度是,也就是2。

 

练习3.9(小项目):坐标3、4和12能以任意顺序创建一个向量,其长度是整数13。这很不寻常,因为大多数数不是完全平方数,所以长度公式中的平方根通常返回无理数。找出另一组三个整数,以它们为坐标定义的向量也有整数长度。

:下面的代码搜索满足条件的三元组,由小于100(可任意选择)的整数组成,并且整数按降序排列。

def vectors_with_whole_number_length(max_coord=100):
    for x in range(1,max_coord):
        for y in range(1,x+1):
          for z in range(1,y+1):
                if length((x,y,z)).is_integer():
                    yield (x,y,z)

它找到了869个具有整数坐标和整数长度的向量。最短的是(2, 2, 1),长度正好是3;最长的是(99, 90, 70),长度是150。

 

练习3.10:找到一个与(-1, -1, 2)方向相同但长度为1的向量。

提示:找到合适的标量与原向量相乘,以适当地改变其长度。

:(-1, -1, 2)的长度大约是2.45,所以需要把这个向量乘以1/2.45,使其长度为1。

>>> length((-1,-1,2))
2.449489742783178
>>> s = 1/length((-1,-1,2))
>>> scale(s,(-1,-1,2))
(-0.4082482904638631, -0.4082482904638631, 0.8164965809277261)
>>> length(scale(s,(-1,-1,2)))
1.0

将每个坐标四舍五入到最接近的百分位,所求向量为(-0.41, -0.41, 0.82)。