Search code examples
algorithmfibonaccipseudocodepuzzle

How to find the output when using an extremely large input in this algorithm?


During an interview I was given the following problem:

From the following puzzle, what would the output of puzzle(power(2022, 100)) be?

 
function puzzle(N) {
   A, B, C, D = 1, 1, 1, 1
  .repeat N times {
     X = D + 2 * C + 3 * B + 4 * A
     a, b, c, d = b, c, d, x
  }
   return D % 10000000000 
}

From looking at the puzzle and by implementing it on my language of choice, I found out that it forms some kind of Fibonacci sequence. However, the code didn't finish running so it was impossible to me to find the output. I answered that the code could be refactored as a sum of fibs to optimize the output but I wasn't able to do it, however, the interviewer said it was a correct reasoning in the right path (he gave me some more time to crack it but I simply failed).

Now, I'm still feeling curious about it, even after failing the interviewer. Could I get some insight?

edit: thanks to the accepted answer I hacked a solution in Python (apart from the one included in the answer update), here's the code if someone is curious:

#reimplemented in python because Ruby is a real pain to work with matrices

def matrix_pow(matrix, power, modulus):
    # Initialize result to the identity matrix of the same size as matrix
    result = [[int(i == j) for j in range(len(matrix))] for i in range(len(matrix))]
    while power > 0:
        # If the current bit of the exponent is 1, multiply result by matrix
        if power % 2 == 1:
            result = matrix_multiply(result, matrix, modulus)
        # Square matrix and divide power by 2
        matrix = matrix_multiply(matrix, matrix, modulus)
        power //= 2
    # Return the result
    return result

# This function multiplies two matrices modulo a given modulus
def matrix_multiply(matrix1, matrix2, modulus):
    # Initialize result to a matrix of the appropriate size filled with zeros
    result = [[0] * len(matrix2[0]) for _ in range(len(matrix1))]
    # Perform matrix multiplication
    for i in range(len(matrix1)):
        for j in range(len(matrix2[0])):
            for k in range(len(matrix2)):
                result[i][j] += matrix1[i][k] * matrix2[k][j]
                result[i][j] %= modulus
    # Return the result
    return result

# This function solves the puzzle
def puzzle(n):
    # Initialize matrix M
    M = [[0, 1, 0, 0],
         [0, 0, 1, 0],
         [0, 0, 0, 1],
         [4, 3, 2, 1]]
    # Calculate M^n modulo 10^10
    M_pow = matrix_pow(M, n, 10**10)
    # Initialize vector v
    v = [1, 1, 1, 1]
    # Multiply M^n by v to get the solution
    result = matrix_multiply(M_pow, [[x] for x in v], 10**10)
    # Return the element in the fourth row and first column of the result
    return result[3][0]

print(puzzle(10))
# output 30520
print(puzzle(100))
# outputs 720820623
print(puzzle(2022**100))
# outputs 2436815984

Solution

  • I hope you know what matrices are and matrix multiplication

    At all points the status of the puzzle can be written as a column vector like this:

    [A]
    [B]
    [C]
    [D]
    

    After one step it can be written as:

    [B]    [0 1 0 0]    [A]
    [C] -- [0 0 1 0] \/ [B]
    [D] -- [0 0 0 1] /\ [C]
    [X]    [4 3 2 1]    [D]
    

    We can write the right hand side as M * v where M is that 4x4 transition matrix, and v is the vertical vector.

    After N steps we get M^N * v. We know our starting vector v. And we know how to pick the answer. We just need a good way to get M^N.

    And for that, well, there is a trick. It isn't hard to work out the series M, M^2, M^4, M^8, .... And with that series and the base 2 representation of 2022^100 you can figure out the answer to the puzzle. Unfortunately the numbers will get very long. But if you do all calculations modulo 10000000000 (meaning you multiply, then take % 10000000000 to get the remainder), then they stay reasonable and you can generate the answer.

    The connection with Fibonacci is that it is computed by a 2x2 transition matrix and you can to identical logic. But I doubt that there is a way to get from Fibonacci to this.

    Incidentally I consider this a fun problem, but not a very good interview question. Because the arbitrary skill that it tests for is not very well correlated with most programming work that you might have to do.


    Here is a Python version. I verified that it agrees with the naive approach for powers up to 100.

    class ModuloValue:
        def __init__ (self, n, m):
            self.modulo = m
            self.value = n % m
    
        def __int__ (self):
            return self.value
    
        def __add__ (self, other):
            return ModuloValue(self.value + int(other), self.modulo)
    
        def __mul__ (self, other):
            return ModuloValue(self.value * int(other), self.modulo)
    
        def __str__ (self):
            return str(self.value)
    
    class Matrix:
        def __init__ (self, rows):
            self.rows = rows
    
        def value (self, i, j):
            return self.rows[i][j]
    
        def __add__ (self, other):
            rows1 = self.rows
            rows2 = other.rows
            rows_out = []
            for i in range(len(rows1)):
                row = []
                for j in range(len(rows1[0])):
                    row.append(rows1[i][j] + rows2[i][j])
                rows_out.append(row)
            return Matrix(rows_out)
    
        def __mul__ (self, other):
            rows1 = self.rows
            rows2 = other.rows
            rows_out = []
            for i in range(len(rows1)):
                row = []
                for k in range(len(rows2[0])):
                    value = rows1[i][0] * rows2[0][k]
                    for j in range(1, len(rows1[0])):
                        value = value + rows1[i][j] * rows2[j][k]
                    row.append(value)
                rows_out.append(row)
            return Matrix(rows_out)
    
        def __str__ (self):
            answer = '['
            for row in self.rows:
                answer += '\n  [' + ', '.join((str(x) for x in row)) + ']'
            return answer + '\n]'
    
        def pow (self, n):
            power = self
            answer = None
            while 0 < n:
                if 1 == n % 2:
                    if answer is None:
                        answer = power
                    else:
                        answer = answer * power
                power = power * power
                n = n // 2
            return answer
    
    mod = 10000000000
    rows = [
        [0, 1, 0, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 1],
        [4, 3, 2, 1],
    ]
    for i in range(len(rows)):
        rows[i] = [ModuloValue(x, mod) for x in rows[i]]
    m = Matrix(rows)
    m_pow = m.pow(2022**100)
    v = [[1], [1], [1], [1]]
    print((m_pow * Matrix(v)).rows[3][0])