Dynamic Programming: Triangle (Fixed Starting Point & Variable Ending Point)
This chapter explores DP on Grids: Triangle Path Sums, focusing on problems characterized by a fixed starting point and a variable ending point. Unlike previous grid problems where movement was restricted to fixed corners ((0, 0) to (n-1, m-1)), triangular grid structures allow multiple valid termination destinations along the bottom row, necessitating a shift in our recursive and tabular mental models.
1. Problem Statement & Grid Topology
We are given a triangular array of numbers (a right-angled triangle) with rows, where the -th row contains elements. Our objective is to find the minimum path sum from the top row down to any element in the bottom row.
Key Rules of Movement:
- When standing at index
jin rowi, you can move to one of two adjacent positions in rowi + 1:- Directly Down: Index
jin rowi + 1(i + 1, j) - Diagonally Right: Index
j + 1in rowi + 1(i + 1, j + 1)
- Directly Down: Index
Visualizing the Triangle (4 Rows Example)
Row 0: [ 1 ]
Row 1: [ 2, 3 ]
Row 2: [ 3, 6, 7 ]
Row 3: [ 8, 9, 10, 10 ]
- Starting Point: Fixed at the top element
(0, 0). - Ending Point: Variable. You can terminate at any column index in the final bottom row (
n - 1).
2. Why Greedy Solutions Fail
Real-Life Analogy: The Mountain Descent
Imagine descending a treacherous mountain shaped like a triangle where each step costs a certain toll. If you use a greedy strategy—always picking the smaller local number immediately below or diagonal to you—you might make a short-sighted choice early on (e.g., picking a 2 instead of a 3) that traps you later in a region of overwhelmingly high numbers.
- Uniformity Violation: Greedy algorithms rely on local uniformity or monotonic trends. In grid problems with erratic numeric distributions, local optimization leads to suboptimal global outcomes.
- Conclusion: We must evaluate all possible paths to guarantee the absolute minimum sum.
3. Direction of Recursion: Top-Down vs. Bottom-Up
In standard grid problems where the destination was fixed at the bottom-right corner (n-1, m-1), we usually started our recursion from the bottom and worked our way back to (0, 0).
Why Bottom-Up Fails Logically Here:
If we attempted to start from the bottom, we would have multiple valid ending/starting points across all columns of the last row. We would have to spawn multiple recursive calls from every single element in row n-1, complicating our base cases and entry points.
The Superior Approach: Top-Down Traversal
- Start:
f(0, 0)(representing the minimum path sum from the top element to any valid column in the last row). - Rationale: By anchoring our start at the unique apex
(0, 0), we can cleanly branch downward and let the base case handle the variable endings when we hit rown - 1.
4. Designing the Recursive Function
To write a complete recursive solution, we follow the standard 3-step dynamic programming recipe:
- Express in Indices: Represent state as
f(i, j)whereiis the current row andjis the current column. - Explore All Paths: Try moving down (
i + 1, j) and diagonally right (i + 1, j + 1). - Take the Minimum: Return the minimum of the two explored paths plus the current cell's value.
Base Cases & Boundary Conditions
- Destination Reached: When
i == n - 1, we have arrived at the bottom row. We simply return the value of the triangle at(n - 1, j). - Out of Bounds: Because we only ever move straight down (
j) or diagonally right (j + 1), we will never exceed the right boundary of the triangle's valid column indices. Thus, no extra out-of-bounds checks are needed.
Recursive Implementation (C++)
int f(int i, int j, vector<vector<int>>& triangle, int n) {
// Base Case: Reached the last row
if (i == n - 1) {
return triangle[i][j];
}
// Explore Down Path
int down = triangle[i][j] + f(i + 1, j, triangle, n);
// Explore Diagonal Right Path
int diagonal = triangle[i][j] + f(i + 1, j + 1, triangle, n);
return min(down, diagonal);
}
- Time Complexity: (Exponential due to duplicate branching for each cell).
- Space Complexity: (Recursion stack space proportional to the height of the triangle).
5. Overlapping Subproblems & Memoization
Drawing the recursion tree reveals that many states f(i, j) are computed multiple times. To eliminate this redundancy, we introduce a DP memoization table of size initialized to -1.
Memoized Implementation (C++)
int f(int i, int j, vector<vector<int>>& triangle, int n, vector<vector<int>>& dp) {
if (i == n - 1) return triangle[i][j];
if (dp[i][j] != -1) return dp[i][j];
int down = triangle[i][j] + f(i + 1, j, triangle, n, dp);
int diagonal = triangle[i][j] + f(i + 1, j + 1, triangle, n, dp);
return dp[i][j] = min(down, diagonal);
}
// Inside main caller:
vector<vector<int>> dp(n, vector<int>(n, -1));
return f(0, 0, triangle, n, dp);
- Time Complexity: (Number of states in a triangular grid ).
- Space Complexity: (DP table) + (Recursion stack space).
6. Converting to Tabulation (Bottom-Up DP)
To avoid Stack Overflow and Time Limit Exceeded (TLE) errors on strict online judges, we convert the top-down memoized solution into an iterative tabulation approach.
Tabulation Strategy:
- Base Case Initialization: Fill the last row of the DP table (
n - 1) directly with the values from the triangle's last row. - Nested Loops: Iterate row
ibackwards fromn - 2down to0. For each row, iterate columnjfrom0up toi(since rowihasi + 1elements). - State Transition: Apply the recurrence relation using previously computed values from row
i + 1.
Tabulation Implementation (C++)
int minimumTotal(vector<vector<int>>& triangle) {
int n = triangle.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
// Step 1: Initialize the base case (last row)
for (int j = 0; j < n; j++) {
dp[n - 1][j] = triangle[n - 1][j];
}
// Step 2: Bottom-up filling from second-last row up to row 0
for (int i = n - 2; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
int down = triangle[i][j] + dp[i + 1][j];
int diagonal = triangle[i][j] + dp[i + 1][j + 1];
dp[i][j] = min(down, diagonal);
}
}
// Step 3: Top cell holds the global minimum path sum
return dp[0][0];
}
7. Space Optimization
Analyzing our tabulation dependency, computing the values for row i only requires data from row i + 1. We do not need to store the entire DP table in memory.
Optimization Strategy:
- Maintain two single-dimensional vectors:
front: Represents rowi + 1(the row below us).curr: Represents rowi(the row we are currently calculating).
- Once row
iis fully computed,currbecomes the newfrontfor the next iteration.
Space-Optimized Implementation (C++)
int minimumTotal(vector<vector<int>>& triangle) {
int n = triangle.size();
vector<int> front(n, 0);
// Initialize front with the last row values
for (int j = 0; j < n; j++) {
front[j] = triangle[n - 1][j];
}
// Iterate upwards
for (int i = n - 2; i >= 0; i--) {
vector<int> curr(n, 0);
for (int j = 0; j <= i; j++) {
int down = triangle[i][j] + front[j];
int diagonal = triangle[i][j] + front[j + 1];
curr[j] = min(down, diagonal);
}
front = curr; // Shift reference rows upward
}
return front[0];
}
- Time Complexity:
- Space Complexity: (Reduced from by maintaining only two active row arrays).
8. Summary & Key Takeaways
- Variable Ending Points: When destination points are variable across a boundary (like the bottom row of a triangle), anchoring your recursion at a fixed start point
(0, 0)is far more intuitive than starting from multiple targets. - Grid Topology: Triangular grids feature rows where the -th row contains columns, restricting loop boundaries dynamically (
j <= i). - Absence of Out-of-Bounds: Because allowed movements are strictly downward (
i+1, j) and diagonal (i+1, j+1), bounds checking is greatly simplified compared to standard rectangular grids. - Greedy Failure: Local minimal selections fail because triangle values lack monotonic uniformity.
- Memoization Translation: Top-down recursion computes states redundantly; adding an memoization table cuts complexity from exponential down to polynomial .
- Tabulation Rule of Thumb: Tabulation works in the reverse direction of recursion. Since recursion goes from
0ton-1, tabulation builds answers fromn-2back up to0. - Space Compression: Because row computations only depend on the immediately adjacent lower row, space complexity can be optimized from to using rolling arrays (
frontandcurr).