Here is one relatively quick and simple implementation of matrix reduction to find a rank mod 2. To clarify, vectors $r_1, ..., r_k$ are linearly independent if and only if the matrix with rows $r_1, ..., r_k$ has rank k.
Matrix representation. I like to represent Z_2-matrices as dictionaries of the form { row index : set of column indices containing 1 }
. This is not too important, but it is convenient to work with, and it can speed things up considerably if you have sparse matrices.
The reduction algorithm. We reduce rows one by one from top to bottom. For each row we try to push the left-most 1 as much to the right as possible by adding previous rows. To this extent, we look at the left-most 1, i.e., the index min(row)
, and check whether some other_row
above has the same value min(row) == min(other_row)
. If it does, we add the other_row
to row
. We repeat until we cannot reduce row
any more. Instead of searching through the rows every time, we store the information in pivot_inverse
, a dictionary of the form {min(row) : row}
. At the end, we have a reduced matrix with each non-zero row having a unique pivot -- the rank of the matrix is the number of pivots, equivalently the number of non-zero rows.
def list_to_sparse(M):
'''Convert list of lists to row sparse matrix representation.'''
return {row_index : set(index for index,value in enumerate(row) if value)
for row_index, row in enumerate(M)}
def rank_Z2(M):
'''Given a Z_2 matrix in row sparse representation, reduce it and return rank.'''
pivot_inverse = {} # remembers which row has a pivot in a given column
for row_index in sorted(M): # we reduce rows one by one from top to bottom
pi = pivot_inverse.get(min(M[row_index], default=-1), -1)
while M[row_index] and pi!=-1:
M[row_index]^= M[pi] # add rows mod 2
pi = pivot_inverse.get(min(M[row_index], default=-1), -1)
if M[row_index]:
pivot_inverse[min(M[row_index])] = row_index
else: # we throw zero rows away
M.pop(row_index)
return len(pivot_inverse) # rank(M) is the number of pivots
Example. For the example from the question, we get the following:
M = [[1,0,1],
[1,1,0],
[0,1,1]]
M_sparse = list_to_sparse(M)
# now M_sparse = {0: {0, 2}, 1: {0, 1}, 2: {1, 2}}
rank_Z2(M_sparse)
# output: 2
# now M_sparse = {0: {0, 2}, 1: {1, 2}}
Performance. I am sure things can be sped up if we use better data types for the job, but the run time is decent. On my laptop a random 1000x1000 (very much not sparse) matrix is reduced in ~10 seconds.
import time
import random
rndM = [[random.randint(0,1) for i in range(1000)] for j in range(1000)]
rndM_sparse = list_to_sparse(rndM)
start_time = time.perf_counter()
rank = rank_Z2(rndM_sparse)
print(f"rank: {rank}")
print(f"time: {time.perf_counter() - start_time : .2f} seconds")
# output:
# rank: 999
# time: 9.82 seconds
We can make rndM
sparser for example like this
rndM = [[1 - bool(random.randint(0,p)) for i in range(1000)] for j in range(1000)]
For p=100
we have around 10000 ones and we get the rank in around 3 seconds, for p=200 we have around 5000 ones and it takes less than a second .
The code also performs better when the rank is low, so for example non-sparse matrix with the same overall size but shaped 100x10000 gets reduced in about 1.5 seconds.