Matrix multiplication consists on given two matrices and , say for now both of dimensions , compute with . The definition implies a naive algorithm of running-time for multiplying two matrices. When I first heard about that, I was somewhat surprised that one can multiply matrices faster then that. The simpler algorithm beating the naive was given by Strassen (1969) and has running-time and is based on a simple observation. Say and break each matrix in 4 blocks of dimensions . So we get something like that:
which are 8 multiplications of smaller matrices. Strassen found a recursion that computes the factors using 7 matrix multiplication, which leads to . This number has been improved various times by Coppersmith and Winograd, Stothers, Vassilevska-Williams and Le Gall. The current best algorithm has running-time . No lower bound is known for this problem other then which is the time needed to read the matrices. The constant is defined as the minimum constant for which it is possible to multiply matrices in time . So far it is known that .
From multiplying matrices to solving linear systems
What I learned this week is that solving linear systems, inverting a matrix and computing determinants can be done also in time with an ingenious idea by Hopcroft and Bunch (1974). Next we show how to do it following the presentation in Mucha and Sankowski (2004). In one line, the idea consists in performing a
The LU decomposition is another way to refer to the Gaussian elimination procedure. We decompose a matrix in the form where L is a lower-diagonal matrix with ones in the diagonal and U is an upper-diagonal matrix with ones in the diagonal. Here are some great lecture notes on the topic, but here is a concise description. The following code is in Octave:
function [L,A] = naive_lu(A) n = length(A); L = eye(n); for i = 1 : n L(i+1:n,i) = A(i+1:n,i) / A(i,i); A(i+1:n,i:n) = A(i+1:n,i:n) - L(i+1:n,i)*A(i,i:n); endfor endfunction
This implementation is very naive in the sense that it doesn’t do pivoting, it doesn’t work for non-singular matrices and so on, but it should be fine for a random matrix, for example. This captures the essence of Gaussian elimination: one makes the matrix triangular by eliminating rows one-by-one. The complexity of the naive implementation is since in each step we update a matrix of size .
The great thing of having an LU decomposition is that now solving is very easy, since solving involves solving and then . Since L and U are both triangular, this can be solved in time. Also, we can compute determinants easily, since . The inverse of A is given by which is twice the time to invert a triangular matrix plus the time to multiply to matrices. The following argument shows that inverting a triangular matrix can be done in the same time as multiplying two matrices: consider an upper triangular matrix U, for example, then:
So, we can solve this system by solving , and since , by pre-multiplying this system by , we get . Therefore, the time to invert a triangular corresponds to two inversions of matrices plus two matrix multiplications, i.e., . Solving the recursion we get .
The previous arguments reduce the problem of computing determinants, solving linear systems and computing inverses to the problem of computing an LU decomposition in time.
Lazy LU decomposition
Note that the expensive step of the LU decomposition consists in updating the matrix by subtracting a rank one matrix. The idea behind the lazy LU decomposition is to postpone the updates such that they are done many row updates are done together in an exponential fashion, i.e., for each a fraction of the updates modifies only rows. The following picture show the traditional LU decomposition:
The k-th iteration, uses entry in order to zero the entries in the blue block marked with . The Hopcroft-Bunch version of the LU factorization zeros the entries in a different fashion, shown in the following picture:
The first few steps work like that: the first step will be almost like the traditional decomposition except that we will use to make entry zero (instead of the entire 1-column): this is done by setting and . Notice that this is the same as doing .
Next we do the same thing for block 2, we perform the same matrix operation: . Notice that at this point will be an upper triangular non-singular matrix. We continue the same procedure until we the entire matrix become upper-diagonal. We store the modifications we performed (i.e., the matrices in the lower diagonal matrix in the same way we did or the usual LU decomposition. The few next steps are:
Generalizing this for an matrix of size we notice that there are operations with matrices of size for to . Each iteration consists of inverting a and a multiplication of a matrix by a matrix, which takes time , since this can be decomposed in multiplications of matrices of size . The overall time is then .
The code for the lazy LU decomposition is surprisingly simple and similar to the code for the traditional LU decomposition. Essentially in each iteration , we compute the size of the block as the maximum value of such that divides (in the code this is done using a bitwise operation).
function [L,A] = lazy_lu(A) n = length(A); L = eye(n); for i = 1 : n-1 g = bitxor(i, bitand(i, i-1)); L(i+1:i+g,i-g+1:i) = A(i+1:i+g,i-g+1:i) * inv(A(i-g+1:i,i-g+1:i)) ; A(i+1:i+g,i-g+1:n) = A(i+1:i+g,i-g+1:n) - L(i+1:i+g,i-g+1:i) * A(i-g+1:i,i-g+1:n); endfor endfunction
The Octave code has running time where is the exponent associated with the matrix multiplication algorithm used. Notice that above we are still using Octave’s native matrix multiplication and inversion, which are . In order to improve on that, one would need to substitute the inversion by a custom inversion (following the receipt given earlier in this post) and the multiplication operations
A(i+1:i+g,i-g+1:i) * inv(A(i-g+1:i,i-g+1:i)) and
L(i+1:i+g,i-g+1:i) * A(i-g+1:i,i-g+1:n) by custom matrix multiplication.
Also, the code below should produce a correct decomposition for a random matrix, which is guaranteed to be well behaved. For specific matrices (like the matrix with all zeros and one in the secondary diagonal), the LU will fail since it will try to invert a singular matrix (the same way that a naive LU decomposition — our first algorithm in this page — will fail). The solution is to do pivoting and write an LUP decomposition, where L and U are as before and is a permutation matrix. Pivoting here can be done as usual, here is a code snippet that is guaranteed to work for any non-singular matrix:
function [L,A,P] = lazy_lu_with_pivoting(A) n = length(A); L = eye(n); perm = [1:n]; for i = 1 : n-1 g = grow(i); f = min(i+g, n); # pivoting step [_, piv] = max(A(i,i:end)); piv = piv + i - 1; if (i != piv) temp = A(:,i); A(:,i) = A(:,piv); A(:,piv) = temp; temp = perm(i); perm(i) = perm(piv); perm(piv) = temp; endif L(i+1:f,i-g+1:i) = A(i+1:f,i-g+1:i) * inv(A(i-g+1:i,i-g+1:i)) ; A(i+1:f,i-g+1:n) = A(i+1:f,i-g+1:n) - L(i+1:f,i-g+1:i) * A(i-g+1:i, i-g+1:n); endfor P = eye(n)(perm, :); endfunction
Lastly, one might wonder if solving a linear system or inverting a matrix is perhaps easier then multiplying two matrices. Hopcroft and Bunch point out the following fact that shows that inverting a matrix in implies an algorithm for multiplying matrices in the same time: