Strassen矩阵乘法
Strassen矩阵乘法
题目描述
给定两个同阶、非空的整数方阵 ,计算矩阵乘积 ,即:
行列下标从0开始。允许负数和0,输入阶数 不必为2的幂;返回新的 矩阵,不修改输入。空矩阵、非方阵或两矩阵阶数不同,抛出 ValueError。代码检查形状,元素为整数是输入前提。
要求使用 Strassen 方法,把普通分块乘法的8次子矩阵乘法减少到7次;若阶数不是2的幂,先补零,再裁剪输出。1×1矩阵直接进行一次标量乘法。
示例:
本题与 Karatsuba 大整数乘法共享代数优化思路:用更多加减法换取更少的递归乘法,降低渐近时间指数。
题目分析
按四个块分解
当阶数为偶数时,将 分为四个等阶方阵 和 。
普通分块公式中,每个结果块都需要两次子矩阵乘法,四个块合计8次。例如 。仅仅四分并不会自动改善 的时间复杂度。
Strassen 构造以下7个乘积,每个乘积递归使用同一算法:
再重组四个结果块:
补零与边界
定义 multiply(a, b):返回两个同阶、阶数为2的幂的矩阵之积。阶数1直接返回标量乘积,否则分块、执行7个子调用、重组。
外层将任意合法阶数 补到不小于它的最小2次幂 。新增行列全部为0,原矩阵放在左上角;补齐乘积的左上 区域就是原乘积,最后只返回这一部分。
正确性依据
展开7个乘积并按重组公式加减,冗余项相互抵消,恰好得到普通分块乘法的四个结果块。推导只使用分配律和加减,没有交换矩阵乘法顺序;一般矩阵乘法并不满足交换律。
标量边界正确,子乘积正确时重组也正确,归纳得到补齐矩阵的正确乘积。补零项对原位置的点积贡献为0,裁剪后仍是 。
实现代码
Python 补零与七分支实现
def strassen(a, b):
n = len(a)
if n == 0 or len(b) != n:
raise ValueError("需要两个同阶非空方阵")
if any(len(row) != n for row in a) or any(len(row) != n for row in b):
raise ValueError("需要两个同阶非空方阵")
def add(x, y, sign=1):
# sign为-1时计算x-y
return [[x[i][j] + sign * y[i][j] for j in range(len(x))]
for i in range(len(x))]
def split(x):
half = len(x) // 2
return (
[row[:half] for row in x[:half]],
[row[half:] for row in x[:half]],
[row[:half] for row in x[half:]],
[row[half:] for row in x[half:]],
)
def multiply(x, y):
if len(x) == 1:
return [[x[0][0] * y[0][0]]]
a11, a12, a21, a22 = split(x)
b11, b12, b21, b22 = split(y)
m1 = multiply(add(a11, a22), add(b11, b22))
m2 = multiply(add(a21, a22), b11)
m3 = multiply(a11, add(b12, b22, -1))
m4 = multiply(a22, add(b21, b11, -1))
m5 = multiply(add(a11, a12), b22)
m6 = multiply(add(a21, a11, -1), add(b11, b12))
m7 = multiply(add(a12, a22, -1), add(b21, b22))
c11 = add(add(add(m1, m4), m5, -1), m7)
c12 = add(m3, m5)
c21 = add(m2, m4)
c22 = add(add(add(m1, m2, -1), m3), m6)
half = len(x) // 2
return ([c11[i] + c12[i] for i in range(half)]
+ [c21[i] + c22[i] for i in range(half)])
size = 1
while size < n:
size *= 2
padded_a = [[0] * size for _ in range(size)]
padded_b = [[0] * size for _ in range(size)]
for i in range(n):
padded_a[i][:n] = a[i][:]
padded_b[i][:n] = b[i][:]
result = multiply(padded_a, padded_b)
return [row[:n] for row in result[:n]]
print(strassen([[1, 2], [3, 4]], [[5, 6], [7, 8]]))
print(strassen([[1, 2, 3], [4, 5, 6], [7, 8, 9]],
[[1, 0, 0], [0, 1, 0], [0, 0, 1]]))
print(strassen([[-2]], [[3]]))
输出:
[[19, 22], [43, 50]]
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[[-6]]
每行都独立创建,不能用 [[0] * size] * size 创建补零矩阵,否则多行会引用同一个列表。整数加减乘法在 Python 中精确执行,没有浮点舍入。
算法执行过程
对2×2主示例,各块均为1×1,7个递归乘法直接到达标量边界。
| 乘积 | 标量计算 | 值 |
|---|---|---|
| 65 | ||
| 35 | ||
| -2 | ||
| 8 | ||
| 24 | ||
| 22 | ||
| -30 |
重组得到:
第二个示例阶数为3,先补成4×4方阵,递归计算后裁剪前三行、前三列。与单位矩阵相乘应恢复原矩阵,可用来检查补零和裁剪是否正确。
复杂度分析
令原阶数为 、补齐阶数为 ,有 ( 时 )。以下按单次标量加减乘法为单位成本;大整数元素的实际运算还取决于其位数。
时间复杂度:
当前层的分块、矩阵加减和拼接均为二次工作。补零与裁剪的 不改变总上界。普通四块乘法执行8个子调用,递推为 ,仍得到 。
辅助空间复杂度: ,包括补齐矩阵、分块、中间乘积和结果矩阵;调用栈深度为 。七个分支顺序执行,沿调用链的同时存活矩阵面积按几何级数递减,不能把空间直接写成七叉树的全部节点数。最终输出另需 空间。
本实现递归到标量,补齐阶数 时,乘法叶子数为 。当 时补到4,做49次标量叶子乘法,比普通3×3算法的27次还多。因此渐近优势不保证小矩阵更快;本篇以清楚展示七乘法结构为目的。
易错点
- 四分后依旧做8次乘法,却声称实现了 Strassen。 关键在于七个乘积和正确的重组公式。
- 将矩阵乘积的左右顺序交换。 分配律可以使用,交换律一般不能使用。
- 在重组时错写负号。 特别核对 中的 和 中的 。
- 奇数阶直接等半划分。 本实现先补零到2的幂,保证每层四个子块同阶。
- 返回补齐后的整个矩阵。 题目要求输出原阶数,需要裁剪。
- 把乘法数量减少理解成所有输入都更快。 增加的加减、复制和补零会影响实际耗时。
课堂练习
- 1×1输入
[[4]]与[[-3]]返回什么? - 2×2主示例中,若遗漏 的 ,会错算成多少?
- 阶数5补齐到几?本实现共有多少个标量乘法叶子?
- 若把七乘法改为普通八乘法,为什么对数递归深度仍不能保证亚立方时间?
参考答案
[[-12]]。- ,而正确值为19。
- 补齐到8,递归有3层分块,叶子数为 。普通5×5三重循环有125次标量乘法,说明补零和小规模常数不能忽略。
- 分支数也参与总工作量。 的叶子数量为 ,因此时间仍为 。
