You are given a triangle array, return the minimum path sum from top to bottom.
For each step, you may move to an adjacent number of the row below. More formally, if you are on index i on the current row, you may move to either index i or index i + 1 on the next row.
Please upgrade to NeetCode Pro to view company tags.
Prerequisites
Before attempting this problem, you should be comfortable with:
Recursion - Breaking down problems into smaller subproblems with base cases
Memoization - Caching recursive results to avoid redundant computation
Dynamic Programming - Building solutions bottom-up using previously computed values
2D Arrays - Working with triangular data structures and understanding index relationships
1. Recursion
Intuition
Starting from the top of the triangle, at each position we can move either directly down or diagonally down-right. We want to find the path from top to bottom with the minimum sum.
This naturally leads to a recursive approach: from position (row, col), we add the current value and recurse to both possible next positions, taking the minimum result. The base case is reaching past the bottom row, where we return 0.
Algorithm
Define a recursive function dfs(row, col) that returns the minimum path sum from that position to the bottom.
Base case: if row exceeds the triangle height, return 0.
Return the current cell value plus the minimum of dfs(row + 1, col) and dfs(row + 1, col + 1).
funcminimumTotal(triangle [][]int)int{var dfs func(row, col int)int
dfs =func(row, col int)int{if row >=len(triangle){return0}return triangle[row][col]+min(dfs(row+1, col),dfs(row+1, col+1))}returndfs(0,0)}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funminimumTotal(triangle: List<List<Int>>): Int {fundfs(row: Int, col: Int): Int {if(row >= triangle.size){return0}return triangle[row][col]+minOf(dfs(row +1, col),dfs(row +1, col +1))}returndfs(0,0)}}
The recursive solution recomputes the same subproblems many times. For example, position (2, 1) might be reached from both (1, 0) and (1, 1). We can use memoization to store results once computed.
By caching the minimum path sum from each position, we ensure each subproblem is solved only once. This transforms the exponential time complexity into polynomial.
Algorithm
Create a memoization table memo initialized with infinity to mark unvisited cells.
Define dfs(row, col) that first checks the memo cache before computing.
If cached, return the stored value immediately.
Otherwise, compute the result recursively, store it in memo, and return it.
publicclassSolution{privateint[][] memo;privateList<List<int>> triangle;privateint INF =int.MaxValue;publicintMinimumTotal(List<List<int>> triangle){this.triangle = triangle;
memo =newint[triangle.Count][];for(int r =0; r < triangle.Count; r++){
memo[r]=newint[triangle[r].Count];for(int c =0; c < triangle[r].Count; c++){
memo[r][c]= INF;}}returnDfs(0,0);}privateintDfs(int row,int col){if(row >= triangle.Count){return0;}if(memo[row][col]!= INF){return memo[row][col];}
memo[row][col]= triangle[row][col]+ Math.Min(Dfs(row +1, col),Dfs(row +1, col +1));return memo[row][col];}}
funcminimumTotal(triangle [][]int)int{
n :=len(triangle)
memo :=make([][]int, n)for r :=0; r < n; r++{
memo[r]=make([]int,len(triangle[r]))for c :=0; c <len(triangle[r]); c++{
memo[r][c]= math.MaxInt32
}}var dfs func(row, col int)int
dfs =func(row, col int)int{if row >= n {return0}if memo[row][col]!= math.MaxInt32 {return memo[row][col]}
memo[row][col]= triangle[row][col]+min(dfs(row+1, col),dfs(row+1, col+1))return memo[row][col]}returndfs(0,0)}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funminimumTotal(triangle: List<List<Int>>): Int {val n = triangle.size
val memo =Array(n){ r ->IntArray(triangle[r].size){ Int.MAX_VALUE }}fundfs(row: Int, col: Int): Int {if(row >= n)return0if(memo[row][col]!= Int.MAX_VALUE)return memo[row][col]
memo[row][col]= triangle[row][col]+minOf(dfs(row +1, col),dfs(row +1, col +1))return memo[row][col]}returndfs(0,0)}}
classSolution{funcminimumTotal(_ triangle:[[Int]])->Int{let n = triangle.count
var memo = triangle.map {$0.map {_inInt.max }}funcdfs(_ row:Int,_ col:Int)->Int{if row >= n {return0}if memo[row][col]!=Int.max {return memo[row][col]}
memo[row][col]= triangle[row][col]+min(dfs(row +1, col),dfs(row +1, col +1))return memo[row][col]}returndfs(0,0)}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(n2)
3. Dynamic Programming (Bottom-Up)
Intuition
Instead of recursing from top to bottom, we can build the solution from the bottom up. Starting from the last row (where the values are the path sums themselves), we work upward. At each cell, we add the minimum of the two cells below it.
This eliminates recursion overhead and naturally fills the dp table in the correct order. By the time we reach the top, dp[0][0] contains the minimum path sum.
Algorithm
Create a dp table with the same shape as the triangle.
Initialize the bottom row of dp with the bottom row of the triangle.
Iterate from the second-to-last row up to the first row.
For each cell, set dp[row][col] = triangle[row][col] + min(dp[row+1][col], dp[row+1][col+1]).
publicclassSolution{publicintminimumTotal(List<List<Integer>> triangle){int n = triangle.size();int[][] dp =newint[n][n];for(int col =0; col < triangle.get(n -1).size(); col++){
dp[n -1][col]= triangle.get(n -1).get(col);}for(int row = n -2; row >=0; row--){for(int col =0; col < triangle.get(row).size(); col++){
dp[row][col]= triangle.get(row).get(col)+Math.min(dp[row +1][col], dp[row +1][col +1]);}}return dp[0][0];}}
classSolution{public:intminimumTotal(vector<vector<int>>& triangle){int n = triangle.size();
vector<vector<int>>dp(n,vector<int>(n,0));for(int col =0; col < triangle[n -1].size();++col){
dp[n -1][col]= triangle[n -1][col];}for(int row = n -2; row >=0;--row){for(int col =0; col < triangle[row].size();++col){
dp[row][col]= triangle[row][col]+min(dp[row +1][col], dp[row +1][col +1]);}}return dp[0][0];}};
classSolution{/**
* @param {number[][]} triangle
* @return {number}
*/minimumTotal(triangle){const n = triangle.length;const dp = Array.from({length: n },(_, i)=>Array(triangle[i].length).fill(0),);for(let col =0; col < triangle[n -1].length; col++){
dp[n -1][col]= triangle[n -1][col];}for(let row = n -2; row >=0; row--){for(let col =0; col < triangle[row].length; col++){
dp[row][col]=
triangle[row][col]+
Math.min(dp[row +1][col], dp[row +1][col +1]);}}return dp[0][0];}}
publicclassSolution{publicintMinimumTotal(List<List<int>> triangle){int n = triangle.Count;int[][] dp =newint[n][];for(int i =0; i < n; i++){
dp[i]=newint[triangle[i].Count];}for(int c =0; c < triangle[n -1].Count; c++){
dp[n -1][c]= triangle[n -1][c];}for(int row = n -2; row >=0; row--){for(int col =0; col < triangle[row].Count; col++){
dp[row][col]= triangle[row][col]+ Math.Min(dp[row +1][col], dp[row +1][col +1]);}}return dp[0][0];}}
funcminimumTotal(triangle [][]int)int{
n :=len(triangle)
dp :=make([][]int, n)for i :=0; i < n; i++{
dp[i]=make([]int,len(triangle[i]))}for col :=0; col <len(triangle[n-1]); col++{
dp[n-1][col]= triangle[n-1][col]}for row := n -2; row >=0; row--{for col :=0; col <len(triangle[row]); col++{
dp[row][col]= triangle[row][col]+min(dp[row+1][col], dp[row+1][col+1])}}return dp[0][0]}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funminimumTotal(triangle: List<List<Int>>): Int {val n = triangle.size
val dp =Array(n){ i ->IntArray(triangle[i].size)}for(col in triangle[n -1].indices){
dp[n -1][col]= triangle[n -1][col]}for(row in n -2 downTo 0){for(col in triangle[row].indices){
dp[row][col]= triangle[row][col]+minOf(dp[row +1][col], dp[row +1][col +1])}}return dp[0][0]}}
classSolution{funcminimumTotal(_ triangle:[[Int]])->Int{let n = triangle.count
var dp = triangle.map {$0.map {_in0}}for col in0..<triangle[n -1].count {
dp[n -1][col]= triangle[n -1][col]}for row instride(from: n -2, through:0, by:-1){for col in0..<triangle[row].count {
dp[row][col]= triangle[row][col]+min(dp[row +1][col], dp[row +1][col +1])}}return dp[0][0]}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(n2)
4. Dynamic Programming (Space Optimized) - I
Intuition
When building top-down, we only need the previous row to compute the current row. We can use a single array that grows as we move down the triangle, updating values as we go.
The tricky part is handling the edges correctly. The leftmost element of each row can only come from the leftmost element above. The rightmost can only come from the rightmost above. Middle elements take the minimum of their two parents.
Algorithm
Initialize dp with the first row of the triangle.
For each subsequent row, create a new nxtDp array of appropriate size.
Set the first element as dp[0] + triangle[row][0].
For middle elements, take triangle[row][col] + min(dp[col], dp[col-1]).
Set the last element as dp[last] + triangle[row][last].
Working bottom-up with space optimization is cleaner because we process elements left to right, and each cell only depends on cells to its right in the row below. This means we can safely overwrite values as we go without corrupting data we still need.
We start with the bottom row and repeatedly update each position with the minimum path sum from that point down. The final answer ends up in dp[0].
Algorithm
Initialize dp as a copy of the bottom row.
Iterate from the second-to-last row up to the first.
For each position in the current row, update dp[col] = triangle[row][col] + min(dp[col], dp[col+1]).
Since we process left to right and dp[col+1] is accessed before dp[col] is overwritten, no data is lost.
classSolution:defminimumTotal(self, triangle: List[List[int]])->int:
n =len(triangle)
dp = triangle[-1][:]for row inrange(n -2,-1,-1):for col inrange(len(triangle[row])):
dp[col]= triangle[row][col]+min(dp[col], dp[col +1])return dp[0]
publicclassSolution{publicintminimumTotal(List<List<Integer>> triangle){int n = triangle.size();int[] dp =newint[n];for(int i =0; i < n; i++){
dp[i]= triangle.get(n -1).get(i);}for(int row = n -2; row >=0; row--){for(int col =0; col < triangle.get(row).size(); col++){
dp[col]= triangle.get(row).get(col)+Math.min(dp[col], dp[col +1]);}}return dp[0];}}
classSolution{public:intminimumTotal(vector<vector<int>>& triangle){int n = triangle.size();
vector<int>dp(triangle.back());for(int row = n -2; row >=0;--row){for(int col =0; col < triangle[row].size();++col){
dp[col]= triangle[row][col]+min(dp[col], dp[col +1]);}}return dp[0];}};
classSolution{/**
* @param {number[][]} triangle
* @return {number}
*/minimumTotal(triangle){const n = triangle.length;const dp =[...triangle[n -1]];for(let row = n -2; row >=0; row--){for(let col =0; col < triangle[row].length; col++){
dp[col]= triangle[row][col]+ Math.min(dp[col], dp[col +1]);}}return dp[0];}}
publicclassSolution{publicintMinimumTotal(List<List<int>> triangle){int n = triangle.Count;int[] dp =newint[triangle[n -1].Count];for(int i =0; i < triangle[n -1].Count; i++){
dp[i]= triangle[n -1][i];}for(int row = n -2; row >=0; row--){for(int col =0; col < triangle[row].Count; col++){
dp[col]= triangle[row][col]+ Math.Min(dp[col], dp[col +1]);}}return dp[0];}}
funcminimumTotal(triangle [][]int)int{
n :=len(triangle)
dp :=make([]int,len(triangle[n-1]))copy(dp, triangle[n-1])for row := n -2; row >=0; row--{for col :=0; col <len(triangle[row]); col++{
dp[col]= triangle[row][col]+min(dp[col], dp[col+1])}}return dp[0]}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funminimumTotal(triangle: List<List<Int>>): Int {val n = triangle.size
val dp = triangle[n -1].toIntArray()for(row in n -2 downTo 0){for(col in triangle[row].indices){
dp[col]= triangle[row][col]+minOf(dp[col], dp[col +1])}}return dp[0]}}
classSolution{funcminimumTotal(_ triangle:[[Int]])->Int{let n = triangle.count
var dp = triangle[n -1]for row instride(from: n -2, through:0, by:-1){for col in0..<triangle[row].count {
dp[col]= triangle[row][col]+min(dp[col], dp[col +1])}}return dp[0]}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(n) extra space.
6. Dynamic Programming (In-Place)
Intuition
If we are allowed to modify the input triangle, we can skip creating a separate DP array entirely. We use the triangle itself to store intermediate results, applying the same bottom-up logic.
This approach uses constant extra space but modifies the input data. Each cell in the triangle gets replaced with the minimum path sum from that cell to the bottom.
Algorithm
Iterate from the second-to-last row up to the first.
For each cell, update triangle[row][col] += min(triangle[row+1][col], triangle[row+1][col+1]).
The update modifies the current cell based on the two cells directly below it.
After all iterations, triangle[0][0] contains the minimum path sum.
funcminimumTotal(triangle [][]int)int{for row :=len(triangle)-2; row >=0; row--{for col :=0; col <len(triangle[row]); col++{
triangle[row][col]+=min(triangle[row+1][col], triangle[row+1][col+1])}}return triangle[0][0]}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funminimumTotal(triangle: MutableList<MutableList<Int>>): Int {for(row in triangle.size -2 downTo 0){for(col in triangle[row].indices){
triangle[row][col]+=minOf(triangle[row +1][col], triangle[row +1][col +1])}}return triangle[0][0]}}
Using Top-Down DP Without Tracking the Minimum at the Bottom
When using top-down dynamic programming, the minimum path sum ends up at one of the cells in the last row, not necessarily at a fixed position. A common mistake is to return dp[n-1][0] or dp[n-1][n-1] instead of finding the minimum across the entire bottom row. The bottom-up approach avoids this by naturally propagating the answer to dp[0][0].
Incorrect Index Handling for Adjacent Cells
In the triangle, a cell at position (row, col) can only move to (row+1, col) or (row+1, col+1). A common error is to treat this like a standard grid where you can move to col-1 as well. Since each row has exactly row+1 elements, the valid adjacent indices are strictly col and col+1 in the next row.
Forgetting to Handle Single-Element Triangles
When the triangle has only one row with a single element, some implementations may fail if they assume there are at least two rows. Always ensure your base case or loop bounds correctly handle the edge case where n == 1, returning the single element as the answer.