<p>You are given <code>n</code> balloons, indexed from <code>0</code> to <code>n - 1</code>. Each balloon is painted with a number on it represented by an array <code>nums</code>. You are asked to burst all the balloons.</p>

<p>If you burst the <code>i<sup>th</sup></code> balloon, you will get <code>nums[i - 1] * nums[i] * nums[i + 1]</code> coins. If <code>i - 1</code> or <code>i + 1</code> goes out of bounds of the array, then treat it as if there is a balloon with a <code>1</code> painted on it.</p>

<p>Return <em>the maximum coins you can collect by bursting the balloons wisely</em>.</p>

<p>&nbsp;</p>
<p><strong class="example">Example 1:</strong></p>

<pre>
<strong>Input:</strong> nums = [3,1,5,8]
<strong>Output:</strong> 167
<strong>Explanation:</strong>
nums = [3,1,5,8] --&gt; [3,5,8] --&gt; [3,8] --&gt; [8] --&gt; []
coins =  3*1*5    +   3*5*8   +  1*3*8  + 1*8*1 = 167</pre>

<p><strong class="example">Example 2:</strong></p>

<pre>
<strong>Input:</strong> nums = [1,5]
<strong>Output:</strong> 10
</pre>

<p>&nbsp;</p>
<p><strong>Constraints:</strong></p>

<ul>
	<li><code>n == nums.length</code></li>
	<li><code>1 &lt;= n &lt;= 300</code></li>
	<li><code>0 &lt;= nums[i] &lt;= 100</code></li>
</ul>

r'''
# Requirements

n = len(nums)
1 <= n <= 300
0 <= nums[i] <= 100

# Edge cases

1. n == 1
2. nums[i] == 0
3. Bursting balloons on the boundary
    - Treat it as if there are balloons of value 1 on the boundary

# Solutions

1. Brute force, backtracking
    - O(n! * n) time, O(n) auxiliary space

2. Memo / DP, bitmasking
    - O(2^n * n) time, O(2^n) auxiliary space

3. Matrix chain ordering problem
    - O(n^3) time, O(n^2) auxiliary space

      (1,3) (3,1) (1,5) (5,8) (8,1)
(1,3)   0     3    30   159   167
(3,1)         0    15   135   159
(1,5)               0    40    48
(5,8)                     0    40
(8,1)                           0

(1,3) | (3,1) (1,5) = 15 +  15 = 30
(1,3) (3,1) | (1,5) = 3  +   5 = 8
(3,1) | (1,5) (5,8) = 40 +  24 = 64
(3,1) (1,5) | (5,8) = 15 + 120 = 135
(1,5) | (5,8) (8,1) = 40 +   5 = 45
(1,5) (5,8) | (8,1) = 40 +   8 = 48
(1,3) | (3,1) (1,5) (5,8) = 135 + 24 = 159
(1,3) (3,1) | (1,5) (5,8) = 3 + 40 + 8 = 51
(1,3) (3,1) (1,5) | (5,8) = 30 + 40 = 70
(3,1) | (1,5) (5,8) (8,1) = 48 + 3 = 51
(3,1) (1,5) | (5,8) (8,1) = 15 + 40 + 15 = 70
(3,1) (1,5) (5,8) | (8,1) = 135 + 24 = 159
(1,3) | (3,1) (1,5) (5,8) (8,1) = 159 + 3 = 162
(1,3) (3,1) | (1,5) (5,8) (8,1) = 3 + 48 + 1 = 52
(1,3) (3,1) (1,5) | (5,8) (8,1) = 30 + 40 + 5 = 75
(1,3) (3,1) (1,5) (5,8) | (8,1) = 159 + 8 = 167
'''
def derive_matrix_sizes(nums: list[int]) -> list[(int, int)]:
    n = len(nums)
    matrices = [(1, nums[0])]

    for i in range(n - 1):
        matrices.append((nums[i], nums[i + 1]))

    matrices.append((nums[-1], 1))
    return matrices

def max_coins_memo(nums: list[int]) -> int:
    matrices = derive_matrix_sizes(nums)
    m = len(matrices)
    memo = [[-1] * m for _ in range(m)]

    def max_ops(l: int, r: int) -> int:
        if l == r:
            return 0
        if memo[l][r] > -1:
            return memo[l][r]
        if l + 1 == r:
            return matrices[l][0] * matrices[r][0] * matrices[r][1]

        for m in range(l + 1, r + 1):
            ops = matrices[l][0] * matrices[m][0] * matrices[r][1] + max_ops(l, m - 1) + max_ops(m, r)
            memo[l][r] = max(memo[l][r], ops)

        return memo[l][r]

    return max_ops(0, m - 1)

class Solution:
    def maxCoins(self, nums: list[int]) -> int:
        return max_coins_memo(nums)
20965