pythonmatrixdiagonal

Get all the diagonals in a matrix/list of lists in Python


I'm looking for a Pythonic way to get all the diagonals of a (square) matrix, represented as a list of lists.

Suppose I have the following matrix:

matrix = [[-2,  5,  3,  2],
          [ 9, -6,  5,  1],
          [ 3,  2,  7,  3],
          [-1,  8, -4,  8]]

Then the large diagonals are easy:

l = len(matrix[0])
print([matrix[i][i] for i in range(l)])              # [-2, -6, 7,  8]
print([matrix[l-1-i][i] for i in range(l-1,-1,-1)])  # [ 2,  5, 2, -1]

But I have trouble coming up with a way to generate all the diagonals. The output I'm looking for is:

[[-2], [9, 5], [3,-6, 3], [-1, 2, 5, 2], [8, 7, 1], [-4, 3], [8],
 [2], [3,1], [5, 5, 3], [-2, -6, 7, 8], [9, 2, -4], [3, 8], [-1]]

Solution

  • I came across another interesting solution to this issue. The row, column, forward, and backward diagonal can all be immediately discovered by looking at a combination of x and y.

    Column = x     Row = y        F-Diag = x+y   B-Diag = x-y     B-Diag` = x-y-MIN 
      | 0  1  2      | 0  1  2      | 0  1  2      | 0  1  2        | 0  1  2     
    --|---------   --|---------   --|---------   --|---------     --|---------    
    0 | 0  1  2    0 | 0  0  0    0 | 0  1  2    0 | 0  1  2      0 | 2  3  4     
    1 | 0  1  2    1 | 1  1  1    1 | 1  2  3    1 |-1  0  1      1 | 1  2  3     
    2 | 0  1  2    2 | 2  2  2    2 | 2  3  4    2 |-2 -1  0      2 | 0  1  2     
    

    From the diagram you can see that each diagonal and axis is uniquely identifiable using these equations. Take each unique number from each table and create a container for that identifier.

    Note that the backward diagonals have been offset to start at a zero index, and that the length of forward diagonals is always equal to the length of backward diagonals.

    test = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]]
    
    max_col = len(test[0])
    max_row = len(test)
    cols = [[] for _ in range(max_col)]
    rows = [[] for _ in range(max_row)]
    fdiag = [[] for _ in range(max_row + max_col - 1)]
    bdiag = [[] for _ in range(len(fdiag))]
    min_bdiag = -max_row + 1
    
    for x in range(max_col):
        for y in range(max_row):
            cols[x].append(test[y][x])
            rows[y].append(test[y][x])
            fdiag[x+y].append(test[y][x])
            bdiag[x-y-min_bdiag].append(test[y][x])
    
    print(cols)
    print(rows)
    print(fdiag)
    print(bdiag)
    

    Which will print

    [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
    [[1, 4, 7, 10], [2, 5, 8, 11], [3, 6, 9, 12]]
    [[1], [2, 4], [3, 5, 7], [6, 8, 10], [9, 11], [12]]
    [[10], [7, 11], [4, 8, 12], [1, 5, 9], [2, 6], [3]]
    

    Using a defaultdict and a lambda, this can be generalized further:

    from collections import defaultdict
    
    
    def groups(data, func):
        grouping = defaultdict(list)
        for y in range(len(data)):
            for x in range(len(data[y])):
                grouping[func(x, y)].append(data[y][x])
        return list(map(grouping.get, sorted(grouping)))
    
    
    test = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
    cols = groups(test, lambda x, y: x)
    rows = groups(test, lambda x, y: y)
    fdiag = groups(test, lambda x, y: x + y)
    bdiag = groups(test, lambda x, y: x - y)