F#与数学(II) – 在图形算法中使用矩阵(PartII)
使用矩阵
Matrix是一种可变类型,因此它可以在创建后被修改。当你必须要修改矩阵式,最好的方法就是一次性的修改,修改后将之视为不可变的(例如:当矩阵作为函数结果被返回时,而此函数是使用可变性来实现的)。这样的话,你会在构造是得到好的性能,同时你可以保持代码的其余部分透明地引用它。
用序号访问矩阵元素
下面的代码展示了直接创建一个4x4的邻接矩阵的方法,此邻接矩阵来自于本文介绍部分所讨论的表示图的两个矩阵(尽管后面我们会看到一种更简单的方法来作这个):
1: let m = Matrix.zero 4 4 2: val m : matrix = matrix [ ... ] 3: 4: m.[0, 0] <- m1.[0, 0] 5: val it : unit = () 6: 7: for i in 0 .. 2 do 8: for j in 0 .. 2 do 9: m.[i+1, j+1] <- m2.[i, j] 10: val it : unit = () 11: 12: m 13: val it : matrix = matrix [ [1.0; 0.0; 0.0; 0.0] 14: [0.0; 0.0; 1.0; 1.0] 15: [0.0; 1.0; 0.0; 0.0] 16: [0.0; 1.0; 0.0; 1.0] ]
第一行代码创建了一个4x4大小的矩阵,矩阵的元素初始化为0.0。接下来,我们复制了矩阵m1和m2(前面段落中定义的)中的内容.矩阵m1只包含了一个元素,因此我们直接赋值了。在复制m2中的元素时,我们需要使用两个嵌套的for循环。元素一旦被复制完,我们就使用F# Interactive 将m的值打印出来。就像你看到的一样,创建好的矩阵和在介绍部分创建的矩阵是一样的。
来看看另一个与直接操作矩阵元素有关的示例,让我们写些代码来返回代表此图边的一个List。这个可以通过搜索矩阵中值为1.0来完成。注意:我们只需要搜索位于对角线下方的三角形即可(包括对角线),因为此矩阵是对称的:
1: [ for i in 0 .. 3 do 2: for j in 0 .. i do 3: if (m.[i, j] = 1.0) then 4: yield i, j ] 5: val it : (int * int) list = [(0, 0); (2, 1); (3, 1); (3, 3)]
嵌套循环的上界被限制在变量i当前的值,如此我们就可以从矩阵的左边搜索到对角线。如果矩阵中的一个元素包含1.0,我们就返回指定此边的两个顶点。
在前面章节中用来复制元素而使用的嵌套循环,其实没必要这么复杂的。下面的部分展示了切割法,此方法提供了一个用来解决此问题的一个更好的方法。
使用切割法访问矩阵的各部分
切割法相当于目录搜索,但是却不是指定某个单独的元素,它允许我们指定一个范围。其结果是包含原矩阵部分元素的矩阵。切割法不仅仅可以被使用在读取部分矩阵上,同样也可以用来用一个矩阵来代替此矩阵的部分。
例如,删除原图中的标号为4的边,我们可以只拿原矩阵的前三列和前三行即可实现:
1: m.[0 .. 2, 0 .. 2] 2: val it : Matrix<float> = matrix [ [1.0; 0.0; 0.0] 3: [0.0; 0.0; 1.0] 4: [0.0; 1.0; 0.0] ]
使用切割法的语法和使用目录法来获取矩阵元素相似。唯一的区别在于需要提供一个范围,如:0 .. 2,而不是仅仅一个序号。获取切片后的结果会是一个有此切割法得到的指定的矩阵。如果我们仔细看看其结果,我们可以看到一个拥有3个顶点2条边的图,此图是通过删除原图的第四条边得到的(一条边由顶点1到顶点1,另一条则是连接顶点2和3)。
切割法同样可以被使用在修改矩阵上。下面的例子展示了一种为整副图构造邻接矩阵的一种更优雅地方式,此邻接矩阵来自此图的两个不相连的邻接矩阵。
1: let m = Matrix.zero 4 4 2: m.[0 .. 0, 0 .. 0] <- m1 3: m.[1 .. 3, 1 .. 3] <- m2
在第一个赋值运算中,这个范围描述了一个大小为1x1的矩阵,在第二个赋值运算中,我们重写了这个矩阵,将之赋为3x3的大小。如果给定的范围超出了矩阵的大小,我们会得到一个异常.同时,我们也要注意m.[0,0]和m.[0..0,0..0]之间有很大的区别。在第一种写法中,我们只能获取一个元素,因此这个表达式的类型是float。在第二种写法中,我们可以获取矩阵的一部分元素,这里只是碰巧只包含一个元素罢了。不过此表达式的类型仍然是matrix(矩阵)。
矩阵之间的运算
接下来,我们会看看一些在F# PowerPack中可用的矩阵间的标准运算操作。就像创建矩阵函数一样,这里我们将要讨论的操作均位于Matrix模块中。下面的代码演示了最开始的两个操作:
1: m = Matrix.transpose m 2: val it : bool = true 3: 4: Matrix.trace m 5: val it : float = 2.0
第一个命令测试了矩阵m的转置是否合原矩阵相同。其结果是true,因为此矩阵是对称的(至少对于无向图来说都是对称的,意思是从顶点1岛2的边与从顶点2到1的边是相同的).
第二条命令用来计算所谓的矩阵的迹,即对角线上元素的和.当计算邻接矩阵时,它返回既是起点又是终点的顶点个数。在这里,其结果为2,由于在序号为[0,0]与[3,3]上有一个值为1.0,这就相当于有边分别从顶点1到1和顶点3到3。
下面的例子用来计算矩阵中边的条数。如果矩阵式对称的,那就意味着一些边会出现两次。要得到不重复边的条数,我们需要计算仅位于矩阵对角线下方(或者上方)的三角形中的元素,包含对角线。下面的例子展示了如何使用标准库中的函数来实现它:
1: m |> Matrix.mapi (fun i j value -> 2: if i > j then 0.0 else value) 3: |> Matrix.sum 4: val it : float = 4.0
函数Matrix.mpi根据元素在矩阵中的序号以及其原始值来计算得出新值。如同其他与矩阵相关的标准函数一样,此函数将一个新创建的矩阵作为值返回。在上面的例子中,我们将位于对角线以下的元素全部替换为0。在接下来的处理步骤中,我们将新创建的矩阵中的元素全部求和,最后得到边的条数。
接下章