Bingo in Numpy array

Viewed 230

Say I have a matrix

import numpy as np
A = np.array([[ 1,  2,  3,  4],
              [ 5,  6,  7,  8],
              [ 9, 10, 11, 12],
              [13, 14, 15, 16]])

I want to check if an 1D array is included in this matrix horizontally, vertically or (anti)diagonally. Below is the code that I came up with

def bingo(a, A):
    return np.max(np.diff(np.argwhere(np.isin(A, a)).T))

a1 = np.array([3, 4])         # horizontal
a2 = np.array([1, 5, 9])      # vertical
a3 = np.array([5, 10, 15])    # diagonal
a4 = np.array([13, 10, 7, 4]) # anti-diagonal
assert bingo(a1, A) == bingo(a2, A) == bingo(a3, A) == bingo(a4, A) == True

However, there are 3 major issues with my code:

  1. I don't want to consider the reverse case, which my code does. For example, np.array([4, 3, 2, 1]) should not be considered as being included in the matrix A.

  2. My code won't work if the array only contains 1 element. For example, np.array([1]) should be considered as being included in the matrix A.

  3. My code won't work if the matrix A contains duplicate elements.

Can anyone suggest a general and numpyic way to do this?

3 Answers

Unfortunately I don't think there's a solution without two loops here. Here's what I've came up with:

def get_diagonal(matrix, pos, length):
     diag = []
     for i in range(min(length, matrix.shape[0] - pos[0], matrix.shape[1] - pos[1])):
         diag.append(matrix[pos[0] + i, pos[1] + i])
     return diag

 def bingo(matrix, array):
     for x_pos in range(matrix.shape[1]):
         for y_pos in range(matrix.shape[0]):
             if all(matrix[y_pos : min(len(array)+y_pos, y_pos+matrix.shape[0]), x_pos] == array) and (matrix.shape[0] - y_pos) >= len(array):
                 print('vertical bingo')
             if all(matrix[y_pos, x_pos : min(len(array)+x_pos, x_pos+matrix.shape[1])] == array) and (matrix.shape[1] - x_pos) >= len(array):
                 print('horizonthal bingo')
             diag = get_diagonal(matrix, (y_pos, x_pos), len(array))
             if all(diag == array) and (len(diag) >= len(array)):
                 print('diagonal bingo')

>>> matrix = np.array([[1,2,3], [4,5,6], [7,8,9]])
>>> bingo(matrix, np.array([5,6]))
horizonthal bingo
>>> bingo(matrix, np.array([5,9]))
diagonal bingo

You need to test the rows, the columns and the two diagonals.

All individual cases are easy to perform, you can then combine them with or to have lazy evaluation (i.e., evaluation stops as soon as one match is found):

import numpy as np
A = np.array([[ 1,  2,  3,  4],
              [ 5,  6,  7,  8],
              [ 9, 10, 11, 12],
              [13, 14, 15, 16]])

def bingo(A, test):
    return (# rows
            (A == test).all(1).any()
            # cols
            or (A.T == test).all(1).any()
            # first diagonal
            or (test == A.diagonal()).all()
            # second diagonal
            or (test == np.fliplr(A.T).diagonal()).all()
            )

bingo(A, [1,2,3,4])    # True

bingo(A, [1,2,4,3])    # False

bingo(A, [2,6,10,14])  # True

bingo(A, [4,3,2,1])    # False 

bingo(A, [1,6,11,16])  # True

bingo(A, [13,10,7,4])  # True

Here is a solution for the first two cases

def bingo(a, A):
    if len(a)==1:
        return a in A
    positions = np.argwhere(np.isin(A, a))
    not_reversed = A[positions[0,0],positions[0,1]] == a[0]
    return np.max(np.diff( positions.T )) * not_reversed 

However your use of max may not be foolproof. For example f = np.array([ 1, 2, 6 ]) will be considered as being in A. I'm starting to wonder if the simplest solutions wouldn't be to check for array length, and then check column by column, line by line and diagonal by diagonal...

Related