LeetCode 120 - Triangle
The problem gives us a triangular array of integers and asks us to compute the minimum path sum from the top row to the bottom row. The input is a two-dimensional array called triangle, where: - The first row contains exactly one number.
Difficulty: 🟡 Medium
Topics: Array, Dynamic Programming
Solution
Problem Understanding
The problem gives us a triangular array of integers and asks us to compute the minimum path sum from the top row to the bottom row.
The input is a two-dimensional array called triangle, where:
- The first row contains exactly one number.
- Each subsequent row contains one more element than the previous row.
- From any position in a row, we are allowed to move only to one of two adjacent positions in the next row.
If we are currently at index i in row r, then in row r + 1 we may move to:
- index
i - index
i + 1
Our goal is to choose a path from the top of the triangle to the bottom such that the sum of all chosen numbers is as small as possible.
For example, consider:
2
3 4
6 5 7
4 1 8 3
Starting at 2, we can move down row by row while respecting adjacency rules. The optimal path is:
2 → 3 → 5 → 1
which produces:
2 + 3 + 5 + 1 = 11
The constraints are important:
- The triangle can have up to 200 rows.
- Values may be negative, ranging from
-10^4to10^4.
Since the number of rows can be fairly large, a brute-force search through every possible path quickly becomes too expensive. Also, because negative numbers are allowed, we cannot rely on greedy decisions such as always taking the smaller immediate child, since a locally smaller choice may lead to a much larger total later.
Several edge cases are important:
- A triangle with only one row, such as
[[-10]], where the answer is simply that value. - Triangles containing negative numbers, where the optimal path may intentionally include a larger immediate value to achieve a smaller total later.
- Large triangles, where exponential recursion becomes impractical.
- Cases where multiple paths produce the same minimum sum, where any valid minimum should still be computed correctly.
Approaches
Brute Force Approach
A straightforward way to solve this problem is to explore every possible path from the top to the bottom using recursion.
At each position (row, col), we have exactly two choices:
- Move to
(row + 1, col) - Move to
(row + 1, col + 1)
We recursively compute the minimum path sum for both options and return:
triangle[row][col] + min(left_path, right_path)
This approach is correct because it evaluates all valid paths and chooses the minimum total among them.
However, the problem is that the number of paths grows exponentially. Since every row introduces two choices, the recursion tree becomes extremely large.
For a triangle with n rows, the time complexity becomes approximately O(2^n).
This is far too slow for n = 200.
Key Insight
The important observation is that many subproblems repeat.
For example, suppose two different paths both arrive at the same cell. From that cell onward, the minimum remaining path sum is always identical. Recomputing it repeatedly is wasteful.
This makes the problem a natural fit for dynamic programming.
Instead of exploring paths repeatedly, we can compute the minimum cost from the bottom upward.
The key recurrence is:
dp[col] = triangle[row][col] + min(dp[col], dp[col + 1])
Here:
dp[col]represents the minimum path sum from the current position downward.dp[col]anddp[col + 1]store results from the row below.
By starting from the bottom row and moving upward, we reuse already-computed optimal subproblems.
Even better, we only need one array of size n (number of rows), which satisfies the follow-up requirement of O(n) extra space.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n) |
O(n) |
Recursively explores every possible path |
| Optimal Dynamic Programming | O(n²) |
O(n) |
Bottom-up DP with a single array |
Algorithm Walkthrough
Optimal Bottom-Up Dynamic Programming
- Create a dynamic programming array initialized with the values of the last row.
The last row already represents the minimum cost to reach the bottom from itself, because there are no further moves.
Example:
Triangle:
2
3 4
6 5 7
4 1 8 3
Initial dp:
[4, 1, 8, 3]
- Process rows from the second-last row upward.
For each element, compute:
current value + minimum of the two reachable children
- Update the DP array in place.
For row [6,5,7]:
dp[0] = 6 + min(4,1) = 7
dp[1] = 5 + min(1,8) = 6
dp[2] = 7 + min(8,3) = 10
Updated:
[7, 6, 10, 3]
- Continue upward until the top row.
Row [3,4]:
dp[0] = 3 + min(7,6) = 9
dp[1] = 4 + min(6,10) = 10
Updated:
[9, 10, 10, 3]
- Process the top row.
dp[0] = 2 + min(9,10) = 11
- Return
dp[0].
After processing all rows, the first position contains the minimum path sum from top to bottom.
Why it works
The algorithm works because of the optimal substructure property. The minimum path sum from a cell depends only on the minimum path sums of its two reachable children.
When processing a row, the DP array already contains the correct minimum costs for the row below. Therefore, every update computes the correct optimal value for the current row. By repeatedly moving upward, the top cell eventually contains the globally minimum path sum.
Python Solution
from typing import List
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
dp = triangle[-1][:]
for row in range(len(triangle) - 2, -1, -1):
for col in range(len(triangle[row])):
dp[col] = triangle[row][col] + min(
dp[col],
dp[col + 1]
)
return dp[0]
The implementation begins by copying the last row into dp. This serves as the base case because the minimum cost from the bottom row to itself is simply its own value.
Next, we iterate upward from the second-last row to the first row. For every position, we calculate the current value plus the smaller of its two reachable children in the row below.
The important detail is that the update happens in place. Since we process left to right, dp[col] and dp[col + 1] still represent the previous row's values when needed.
After all rows are processed, dp[0] stores the minimum path sum from the top to the bottom.
Go Solution
func minimumTotal(triangle [][]int) int {
dp := make([]int, len(triangle[len(triangle)-1]))
copy(dp, triangle[len(triangle)-1])
for row := len(triangle) - 2; row >= 0; row-- {
for col := 0; col < len(triangle[row]); col++ {
if dp[col] < dp[col+1] {
dp[col] = triangle[row][col] + dp[col]
} else {
dp[col] = triangle[row][col] + dp[col+1]
}
}
}
return dp[0]
}
The Go implementation follows the same bottom-up dynamic programming strategy.
Since slices are reference-based, we explicitly create a new slice and use copy() to avoid modifying the original last row in triangle.
Instead of using min(), Go uses a simple conditional comparison because there is no built-in integer minimum function.
Integer overflow is not a concern here because the maximum possible sum remains safely within Go's integer range.
Worked Examples
Example 1
Input:
[[2],[3,4],[6,5,7],[4,1,8,3]]
Initial state:
| Step | Row Being Processed | DP State |
|---|---|---|
| Initial | [4,1,8,3] |
[4,1,8,3] |
Process row [6,5,7]:
| Index | Calculation | Result |
|---|---|---|
| 0 | 6 + min(4,1) |
7 |
| 1 | 5 + min(1,8) |
6 |
| 2 | 7 + min(8,3) |
10 |
Updated DP:
[7, 6, 10, 3]
Process row [3,4]:
| Index | Calculation | Result |
|---|---|---|
| 0 | 3 + min(7,6) |
9 |
| 1 | 4 + min(6,10) |
10 |
Updated DP:
[9, 10, 10, 3]
Process row [2]:
| Index | Calculation | Result |
|---|---|---|
| 0 | 2 + min(9,10) |
11 |
Final DP:
[11, 10, 10, 3]
Answer:
11
Example 2
Input:
[[-10]]
Initial DP:
[-10]
There are no additional rows to process.
Return:
-10
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n²) |
We visit every element in the triangle exactly once |
| Space | O(n) |
We store only one DP array of size equal to the number of rows |
Although the triangle contains roughly n² elements for n rows, each element is processed exactly one time. Therefore, the running time is proportional to the total number of cells in the triangle.
The extra memory usage is only one array storing the current DP state, which satisfies the follow-up requirement of O(n) space.
Test Cases
solution = Solution()
assert solution.minimumTotal([[2], [3, 4], [6, 5, 7], [4, 1, 8, 3]]) == 11 # Example case
assert solution.minimumTotal([[-10]]) == -10 # Single element triangle
assert solution.minimumTotal([[1], [2, 3]]) == 3 # Simple two-row triangle
assert solution.minimumTotal([[1], [1, 1], [1, 1, 1]]) == 3 # Uniform values
assert solution.minimumTotal([[5], [-2, -3]]) == 2 # Negative values
assert solution.minimumTotal([[1], [2, 1], [1, 1, 1]]) == 3 # Multiple optimal paths
assert solution.minimumTotal([[10000]]) == 10000 # Maximum positive value
assert solution.minimumTotal([[-10000]]) == -10000 # Minimum negative value
assert solution.minimumTotal([[1], [2, 3], [3, 6, 1], [7, 2, 8, 4]]) == 10 # Mixed path choices
| Test | Why |
|---|---|
[[2],[3,4],[6,5,7],[4,1,8,3]] |
Validates the main example |
[[-10]] |
Tests single-row triangle |
[[1],[2,3]] |
Smallest non-trivial triangle |
| Uniform values | Ensures repeated values work correctly |
| Negative values | Verifies handling of negative numbers |
| Multiple optimal paths | Ensures minimum is computed consistently |
| Maximum positive value | Boundary value test |
| Minimum negative value | Negative boundary test |
| Mixed path choices | Validates non-greedy optimal selection |
Edge Cases
Single Row Triangle
A triangle containing only one row is the smallest valid input. A naive implementation might accidentally assume at least one transition between rows exists.
For example:
[[-10]]
The implementation handles this naturally because the DP array is initialized with the last row, and the outer loop never executes. The result is simply dp[0].
Negative Numbers
Negative values make greedy strategies unreliable. Choosing the smaller immediate child may not lead to the globally optimal solution.
Example:
[
[5],
[-2, -3]
]
The algorithm correctly evaluates both reachable children using dynamic programming and chooses the true minimum total.
Large Triangles
With up to 200 rows, a brute-force recursive solution would become infeasible because of exponential growth in possible paths.
The bottom-up DP solution processes every element exactly once and uses only a single array, making it efficient enough even for the largest valid inputs.