给定两个大小分别为 nxn 的方阵 A 和 B,求它们的乘法矩阵。
朴素方法:以下是两个矩阵相乘的简单方法。
function multiply(A, B, C)
{
for (var i = 0; i < N; i++)
{
for (var j = 0; j < N; j++)
{
C[i][j] = 0;
for (var k = 0; k < N; k++)
{
C[i][j] += A[i][k]*B[k][j];
}
}
}
}
上述方法的时间复杂度为O(N 3 )。
分而治之 :
以下是两个方阵相乘的简单分而治之方法。
1、将矩阵 A 和 B 分为 4 个大小为 N/2 x N/2 的子矩阵,如下图所示。
2、递归计算以下值。 ae + bg、af + bh、ce + dg 和 cf + dh。
执行:
function multiplyMatrix(matrixA, matrixB) {
const n = matrixA.length;
const resultMatrix = new Array(n).fill(null).map(() => new Array(n).fill(0));
if (n === 1) {
resultMatrix[0][0] = matrixA[0][0] * matrixB[0][0];
return resultMatrix;
}
const splitIndex = n / 2;
const a00 = new Array(splitIndex).fill(null).map(() => new Array(splitIndex).fill(0));
const a01 = new Array(splitIndex).fill(null).map(() => new Array(splitIndex).fill(0));
const a10 = new Array(splitIndex).fill(null).map(() => new Array(splitIndex).fill(0));
const a11 = new Array(splitIndex).fill(null).map(() => new Array(splitIndex).fill(0));
const b00 = new Array(splitIndex).fill(null).map(() => new Array(splitIndex).fill(0));
const b01 = new Array(splitIndex).fill(null).map(() => new Array(splitIndex).fill(0));
const b10 = new Array(splitIndex).fill(null).map(() => new Array(splitIndex).fill(0));
const b11 = new Array(splitIndex).fill(null).map(() => new Array(splitIndex).fill(0));
for (let i = 0; i < splitIndex; i++) {
for (let j = 0; j < splitIndex; j++) {
a00[i][j] = matrixA[i][j];
a01[i][j] = matrixA[i][j + splitIndex];
a10[i][j] = matrixA[splitIndex + i][j];
a11[i][j] = matrixA[i + splitIndex][j + splitIndex];
b00[i][j] = matrixB[i][j];
b01[i][j] = matrixB[i][j + splitIndex];
b10[i][j] = matrixB[splitIndex + i][j];
b11[i][j] = matrixB[i + splitIndex][j + splitIndex];
}
}
const resultMatrix00 = addMatrix(multiplyMatrix(a00, b00), multiplyMatrix(a01, b10), splitIndex);
const resultMatrix01 = addMatrix(multiplyMatrix(a00, b01), multiplyMatrix(a01, b11), splitIndex);
const resultMatrix10 = addMatrix(multiplyMatrix(a10, b00), multiplyMatrix(a11, b10), splitIndex);
const resultMatrix11 = addMatrix(multiplyMatrix(a10, b01), multiplyMatrix(a11, b11), splitIndex);
for (let i = 0; i < splitIndex; i++) {
for (let j = 0; j < splitIndex; j++) {
resultMatrix[i][j] = resultMatrix00[i][j];
resultMatrix[i][j + splitIndex] = resultMatrix01[i][j];
resultMatrix[splitIndex + i][j] = resultMatrix10[i][j];
resultMatrix[i + splitIndex][j + splitIndex] = resultMatrix11[i][j];
}
}
return resultMatrix;
}
function addMatrix(matrixA, matrixB, n) {
const resultMatrix = new Array(n).fill(null).map(() => new Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
resultMatrix[i][j] = matrixA[i][j] + matrixB[i][j];
}
}
return resultMatrix;
}
const matrixA = [
[1, 1, 1, 1],
[2, 2, 2, 2],
[3, 3, 3, 3],
[2, 2, 2, 2]
];
console.log("Array A =>")
console.log(matrixA);
const matrixB = [
[1, 1, 1, 1],
[2, 2, 2, 2],
[3, 3, 3, 3],
[2, 2, 2, 2]
];
console.log("Array B =>")
console.log(matrixB);
const resultMatrix = multiplyMatrix(matrixA, matrixB);
console.log("Result Array =>")
console.log(resultMatrix);
输出
数组A =>
1 1 1 1
2 2 2 2
3 3 3 3
2 2 2 2
数组 B =>
1 1 1 1
2 2 2 2
3 3 3 3
2 2 2 2
结果数组=>
8 8 8 8
16 16 16 16
24 24 24 24
16 16 16 16
在上述方法中,我们对大小为 N/2 x N/2 的矩阵进行 8 次乘法和 4 次加法。两个矩阵相加需要 O(N 2 ) 时间。所以时间复杂度可以写成
T(N) = 8T(N/2) + O(N 2 )
根据马斯特定理,上述方法的时间复杂度为 O(N 3 )
不幸的是,这与上面的简单方法相同。
简单的分而治之也导致O(N 3 ),有更好的方法吗?
在上面的分而治之的方法中,高时间复杂度的主要成分是8次递归调用。Strassen 方法的思想是将递归调用次数减少到 7 次。Strassen 方法与上述简单的分而治之方法类似,该方法也将矩阵划分为大小为 N/2 x N/2 的子矩阵:如上图所示,但在Strassen方法中,结果的四个子矩阵是使用以下公式计算的。
Strassen 方法的时间复杂度
两个矩阵的加法和减法需要 O(N 2 ) 时间。所以时间复杂度可以写成
T(N) = 7T(N/2) + O(N 2 )
根据马斯特定理,上述方法的时间复杂度为
O(N Log7 ) 大约为 O(N 2.8074 )
一般来说,由于以下原因,施特拉森方法在实际应用中并不优选。
1、Strassen 方法中使用的常数很高,对于典型应用,Naive 方法效果更好。
2、对于稀疏矩阵,有专门为其设计的更好的方法。
3、递归中的子矩阵占用额外的空间。
4、由于计算机对非整数值的运算精度有限,Strassen 算法中累积的误差比 Naive 方法中更大。
执行:
function split(matrix) {
/*
Splits a given matrix into quarters.
Input: nxn matrix
Output: tuple containing 4 n/2 x n/2 matrices corresponding to a, b, c, d
*/
let row = matrix.length,
col = matrix[0].length;
let row2 = Math.floor(row / 2),
col2 = Math.floor(col / 2);
return [matrix.slice(0, row2).map(x => x.slice(0, col2)),
matrix.slice(0, row2).map(x => x.slice(col2)),
matrix.slice(row2).map(x => x.slice(0, col2)),
matrix.slice(row2).map(x => x.slice(col2))
];
}
function strassen(x, y) {
/*
Computes matrix product by divide and conquer approach, recursively.
Input: nxn matrices x and y
Output: nxn matrix, product of x and y
*/
// Base case when size of matrices is 1x1
if (x.length === 1) {
return [
[x[0][0] * y[0][0]]
];
}
// Splitting the matrices into quadrants. This will be done recursively
// until the base case is reached.
let [a, b, c, d] = split(x);
let [e, f, g, h] = split(y);
// Computing the 7 products, recursively (p1, p2...p7)
let p1 = strassen(a, sub(f, h));
let p2 = strassen(add(a, b), h);
let p3 = strassen(add(c, d), e);
let p4 = strassen(d, sub(g, e));
let p5 = strassen(add(a, d), add(e, h));
let p6 = strassen(sub(b, d), add(g, h));
let p7 = strassen(sub(a, c), add(e, f));
// Computing the values of the 4 quadrants of the final matrix c
let c11 = add(sub(add(p5, p4), p2), p6);
let c12 = add(p1, p2);
let c21 = add(p3, p4);
let c22 = sub(sub(add(p1, p5), p3), p7);
// Combining the 4 quadrants into a single matrix by stacking horizontally and vertically.
let top = c11.map((x, i) => x.concat(c12[i]));
let bottom = c21.map((x, i) => x.concat(c22[i]));
return top.concat(bottom);
}
function add(a, b) {
return a.map((x, i) => x.map((y, j) => y + b[i][j]));
}
function sub(a, b) {
return a.map((x, i) => x.map((y, j) => y - b[i][j]));
}
// Driver's code
let A = [
[1, 1, 1, 1],
[2, 2, 2, 2],
[3, 3, 3, 3],
[2, 2, 2, 2]
];
let B = [
[1, 1, 1, 1],
[2, 2, 2, 2],
[3, 3, 3, 3],
[2, 2, 2, 2]
];
let C = strassen(A, B);
console.log("Array A =>")
console.table(A);
console.log("Array B =>")
console.table(B);
console.log("Result Array =>")
console.table(C);
// This code is contributed by Prajwal Kandekar
输出
数组A =>
1 1 1 1
2 2 2 2
3 3 3 3
2 2 2 2
数组 B =>
1 1 1 1
2 2 2 2
3 3 3 3
2 2 2 2
结果数组=>
8 8 8 8
16 16 16 16
24 24 24 24
16 16 16 16