Problem 345

completed May 27, 2019

Comments

This problem just shows me how important it is to test. I started working on this problem yesterday and found myself getting a bit stuck on it. I was scratching my head because I thought I had a good algorithm but couldn’t figure out why things were working. Well, that’s because there were at least 2 or 3 or more bugs in the code. I started working on this today by writing tests for each method on my Matrix class. Once I did, I was able to pinpoint problems and fix them easily.

This code works, but it’s not something that I’d use more than once. First of all, it’s not all that quick. The problem takes about 32 seconds to complete. While not bad, it’s also not good if you’re wanting to work with a matrix any bit larger than what we’re given for this problem.

Additionally, I’ve been thinking a lot at work lately about good APIs. I’d say that what I have here is not an idea API. Some methods that I have on the Matrix class really shouldn’t be methods on that class and instead should probably just be top level functions. Examples of this being the _next_vertex and _next_vertex_via_rollback methods. There’s nothing in them that is requiring access to the Matrix class itself, so why have it as a method on that class? I didn’t find it all that important to fix this while working on the problem since this is just one off work, but just some thoughts for if I were to actually need this code long term.

Code

given = """
  7  53 183 439 863
497 383 563  79 973
287  63 343 169 583
627 343 773 959 943
767 473 103 699 303
"""

empty = """
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
"""

easy = """
0 0 0 0 5
0 0 0 5 0
0 0 5 0 0
0 5 0 0 0
5 0 0 0 0
"""

problem = """
  7  53 183 439 863 497 383 563  79 973 287  63 343 169 583
627 343 773 959 943 767 473 103 699 303 957 703 583 639 913
447 283 463  29  23 487 463 993 119 883 327 493 423 159 743
217 623   3 399 853 407 103 983  89 463 290 516 212 462 350
960 376 682 962 300 780 486 502 912 800 250 346 172 812 350
870 456 192 162 593 473 915  45 989 873 823 965 425 329 803
973 965 905 919 133 673 665 235 509 613 673 815 165 992 326
322 148 972 962 286 255 941 541 265 323 925 281 601  95 973
445 721  11 525 473  65 511 164 138 672  18 428 154 448 848
414 456 310 312 798 104 566 520 302 248 694 976 430 392 198
184 829 373 181 631 101 969 613 840 740 778 458 284 760 390
821 461 843 513  17 901 711 993 293 157 274  94 192 156 574
 34 124   4 878 450 476 712 914 838 669 875 299 823 329 699
815 559 813 459 522 788 168 586 966 232 308 833 251 631 107
813 883 451 509 615  77 281 613 459 205 380 274 302  35 805
"""

class Matrix(list):

    # Assumptions:
    #  1. The matrix passed to new_matrix_from_str is square.
    #  2. No value in the matrix is greater than 999.

    @staticmethod
    def new_matrix_from_str(matrix_string):
        matrix = Matrix()
        row_list = matrix_string.split('\n')
        for row in row_list:
            item_str_list = row.split()
            if not item_str_list:
                continue
            item_int_list = [1000-int(item) for item in item_str_list]
            matrix.append(item_int_list)
        return matrix

    def calculate_max_sum(self):
        # start with an initial depth of vertexes
        vertexes = [(i, i) for i in range(self.dimention - 1)]
        min_sum = float('inf')
        # the last vertex will be added in the while loop
        i, j = self.dimention - 1, self.dimention - 1

        while i < self.dimention and j < self.dimention:
            vertexes.append((i, j))
            current_sum = self.vertex_sum(vertexes)

            if current_sum < min_sum and len(vertexes) == self.dimention:
                min_sum = current_sum
            if self._vertexes_complete(vertexes):
                break

            i, j = self._next_vertex(vertexes, current_sum > min_sum)

        return self._inverse_sum(min_sum)

    def _next_vertex(self, vertexes, force_rollback):
        if force_rollback:
            next_i, next_j = self._next_vertex_via_rollback(vertexes)
        else:
            next_i, next_j = vertexes[-1][0] + 1, 0
            while next_i < self.dimention and next_j < self.dimention:
                if self.is_available_vertex(vertexes, (next_i, next_j)):
                    break
                next_j += 1
            else:
                next_i, next_j = self._next_vertex_via_rollback(vertexes)
        return next_i, next_j

    def _next_vertex_via_rollback(self, vertexes):
        while vertexes:
            last_i, last_j = vertexes.pop()
            next_i, next_j = last_i, last_j + 1
            while next_j < self.dimention:
                # get the next available item on the row
                if self.is_available_vertex(vertexes, (next_i, next_j)):
                    return next_i, next_j
                next_i, next_j = next_i, next_j + 1
            # if one is not available, continue to pop another
        return next_i, next_j

    def _vertexes_complete(self, vertexes):
        i, j = self.dimention - 1, 0
        while j < self.dimention:
            if (i, j) not in vertexes:
                return False
            i -= 1
            j += 1
        return True

    @property
    def dimention(self):
        return len(self)

    def _inverse_sum(self, min_sum):
        return self.dimention * 1000 - min_sum

    def vertex_sum(self, vertexes):
        return sum(self[i][j] for i, j in vertexes)

    def is_available_vertex(self, existent_vertexes, next_vertex):
        for i, j in existent_vertexes:
            if i == next_vertex[0] or j == next_vertex[1]:
                return False
        return True


if __name__ == '__main__':
    max_sum = Matrix.new_matrix_from_str(problem).calculate_max_sum()
    print('The maximum matrix sum is:', max_sum)

Tests

import pytest

import problem

_test_matrix = """
 1  2  3  4
 5  6  7  8
 9 10 11 12
13 14 15 16
"""

def test_new_matrix_from_str():
    matrix = problem.Matrix.new_matrix_from_str(_test_matrix)
    assert matrix == [
            [999, 998, 997, 996],
            [995, 994, 993, 992],
            [991, 990, 989, 988],
            [987, 986, 985, 984],
    ]

def test_dimention():
    matrix = problem.Matrix.new_matrix_from_str(_test_matrix)
    assert matrix.dimention == 4

def test__inverse_sum():
    matrix = problem.Matrix.new_matrix_from_str(_test_matrix)
    assert matrix._inverse_sum(123) == 3877

@pytest.mark.parametrize('vertexes,expected', (
    ([(0, 0)], 999),
    ([(0, 0), (1, 1)], 1993),
))
def test_vertex_sum(vertexes, expected):
    matrix = problem.Matrix.new_matrix_from_str(_test_matrix)
    assert matrix.vertex_sum(vertexes) == expected

@pytest.mark.parametrize('vertexes,vertex,expected', (
    ([(0, 0)], (0, 0), False),
    ([(0, 0)], (1, 0), False),
    ([(0, 0)], (0, 1), False),
    ([(0, 0), (1, 1)], (0, 1), False),
    ([(0, 0), (1, 1)], (2, 3), True),
))
def test_is_available_vertex(vertexes, vertex, expected):
    matrix = problem.Matrix.new_matrix_from_str(_test_matrix)
    assert matrix.is_available_vertex(vertexes, vertex) is expected

@pytest.mark.parametrize('vertexes,expected', (
    ([(0, 0)], False),
    ([(3, 3)], False),
    ([(3, 3), (2, 2)], False),
    ([(3, 0), (2, 1), (0, 2), (1, 3)], False),
    ([(3, 0), (2, 1), (1, 2), (0, 3)], True),
))
def test__vertexes_complete(vertexes, expected):
    matrix = problem.Matrix.new_matrix_from_str(_test_matrix)
    assert matrix._vertexes_complete(vertexes) is expected

@pytest.mark.parametrize('vertexes,expected_return,expected_vertexes', (
    ([(0, 0)], (0, 1), []),
    ([(0, 0), (1, 1)], (1, 2), [(0, 0)]),
    ([(0, 2), (1, 3)], (0, 3), []),
    ([(0, 1), (1, 3)], (0, 2), []),
    ([(0, 1), (1, 3), (2, 2)], (0, 2), []),
    ([(0, 0), (1, 1), (2, 2), (3, 3)], (2, 3), [(0, 0), (1, 1)]),
))
def test__next_vertex_via_rollback(vertexes, expected_return, expected_vertexes):
    matrix = problem.Matrix.new_matrix_from_str(_test_matrix)
    actual = matrix._next_vertex_via_rollback(vertexes)
    assert vertexes == expected_vertexes
    assert actual == expected_return

@pytest.mark.parametrize('vertexes,force_rollback,expected_return,expected_vertexes', (
    ([(0, 0)], True, (0, 1), []),
    ([(0, 0), (1, 1)], True, (1, 2), [(0, 0)]),
    ([(0, 2), (1, 3)], True, (0, 3), []),
    ([(0, 1), (1, 3)], True, (0, 2), []),
    ([(0, 1), (1, 3), (2, 2)], True, (0, 2), []),
    ([(0, 0), (1, 1), (2, 2), (3, 3)], True, (2, 3), [(0, 0), (1, 1)]),

    ([(0, 0)], False, (1, 1), [(0, 0)]),
    ([(0, 0), (1, 1)], False, (2, 2), [(0, 0), (1, 1)]),
    ([(0, 2), (1, 3)], False, (2, 0), [(0, 2), (1, 3)]),
    ([(0, 1), (1, 3)], False, (2, 0), [(0, 1), (1, 3)]),
    ([(0, 1), (1, 3), (2, 2)], False, (3, 0), [(0, 1), (1, 3), (2, 2)]),
    ([(0, 2), (1, 3), (2, 1)], False, (3, 0), [(0, 2), (1, 3), (2, 1)]),
    ([(0, 0), (1, 1), (2, 2), (3, 3)], False, (2, 3), [(0, 0), (1, 1)]),
    ([(0, 1), (1, 2), (2, 3), (3, 0)], False, (1, 3), [(0, 1)]),
))
def test__next_vertex(vertexes, force_rollback, expected_return, expected_vertexes):
    matrix = problem.Matrix.new_matrix_from_str(_test_matrix)
    actual = matrix._next_vertex(vertexes, force_rollback)
    assert vertexes == expected_vertexes
    assert actual == expected_return

@pytest.mark.parametrize('test_matrix,expected', (
    (problem.empty, 0),
    (problem.easy, 25),
    (problem.given, 3315),
))
def test_calculate_max_sum(test_matrix, expected):
    matrix = problem.Matrix.new_matrix_from_str(test_matrix)
    assert matrix.calculate_max_sum() == expected