Pascal's Triangle — From Combinatorics to DP Mastery [Easy]
Advertisement
Problem Statement
LeetCode 118 — Pascal's Triangle
Given an integer numRows, return the first numRows rows of Pascal's triangle. In Pascal's triangle, each number is the sum of the two numbers directly above it.
Example:
Input: numRows = 5
Output: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
Row 0: 1
Row 1: 1 1
Row 2: 1 2 1
Row 3: 1 3 3 1
Row 4: 1 4 6 4 1
- Why This Problem Matters
- The Mathematical Background
- The DP Approach
- The Interesting Properties
- Pascal's Triangle II — Single Row in O(k) Space
- Approach 1: In-Place Right-to-Left Update
- Approach 2: Direct Formula — O(rowIndex) time, O(rowIndex) space
- Visual Dry Run
- Common Mistakes
- Solutions
- LeetCode 118 — Generate Pascal's Triangle
- LeetCode 119 — Return Only the k-th Row
- Complexity Analysis
- LC 118 — Generate Full Triangle
- LC 119 — Single Row
- Follow-Up Questions
- This Pattern Solves
- Key Takeaway
Why This Problem Matters
Pascal's Triangle is one of the best first DP problems because the recurrence relation is completely obvious from the visual structure. You can see with your own eyes that every interior cell is the sum of the two cells above it — there is no guesswork, no tricky state definition, and no multi-dimensional array needed for the transition.
That simplicity makes it the perfect bridge from "I've heard of dynamic programming" to "I understand what DP actually means." The structure teaches you three foundational DP habits at once:
- Build on previously computed results. Each row depends entirely on the previous row. You never recompute anything from scratch.
- Think in terms of subproblems. Generating row
iis a subproblem of generating rowi+1. Solve smaller cases first, store them, build up. - Identify base cases explicitly. The first and last element of every row are always
1. Recognizing invariants like these is a core DP skill that carries directly into harder problems like Unique Paths, Edit Distance, and Longest Common Subsequence.
Beyond DP, this triangle sits at the intersection of combinatorics, probability, algebra, and number theory — making it one of the most studied mathematical objects of all time.
The Mathematical Background
Every element in Pascal's Triangle is a binomial coefficient. Specifically, the element in row n at column k (both zero-indexed) equals:
triangle[n][k] = C(n, k) = n! / (k! × (n-k)!)
This is read as "n choose k" — the number of ways to choose k items from n items without regard to order. A few concrete examples from row 4:
C(4,0) = 1 ← the leading 1
C(4,1) = 4 ← 4 ways to pick 1 from 4
C(4,2) = 6 ← 6 ways to pick 2 from 4
C(4,3) = 4
C(4,4) = 1 ← the trailing 1
The DP recurrence triangle[n][k] = triangle[n-1][k-1] + triangle[n-1][k] is just Pascal's Rule for binomial coefficients:
C(n, k) = C(n-1, k-1) + C(n-1, k)
Why does this identity hold? Imagine you have n items and you want to choose k of them. Fix one special item — call it item X. Every valid selection either includes X (then you choose the remaining k-1 from the other n-1 items → C(n-1, k-1) ways) or excludes X (then you choose all k from the other n-1 items → C(n-1, k) ways). Add them together and you get C(n, k). That combinatorial argument is exactly what the triangle's addition rule encodes visually.
This connection means Pascal's Triangle isn't just a coding exercise — it is a concrete, visual proof of Pascal's Rule that you can point to in any interview.
The DP Approach
The algorithm is a direct translation of the recurrence:
Step 1: Start with an empty result list.
Step 2: For each row i from 0 to numRows - 1:
- Create a new row of length
i + 1, initialized entirely to1. - Loop through interior positions
jfrom1toi - 1(exclusive on both ends). - Set
row[j] = prev_row[j-1] + prev_row[j]. - Append the row to the result.
Why initialize to 1? Because the first and last positions are always 1. By filling the whole row with 1 upfront, the loop only needs to handle interior positions — clean and minimal code.
Why loop from j = 1 to j < i? Position 0 and position i are the edge elements, which stay 1. The interior range is [1, i-1] inclusive, which in a half-open loop is range(1, i) or j = 1; j < i.
The Interesting Properties
Understanding these properties makes your blog posts and interview answers far more compelling — and some of them appear directly in other LeetCode problems:
1. Row sum = 2^n The sum of all elements in row n (zero-indexed) is exactly 2^n. Row 0 sums to 1 = 2^0, row 1 sums to 2 = 2^1, row 4 sums to 1+4+6+4+1 = 16 = 2^4. The proof: in (1+1)^n = 2^n, the binomial theorem expands to the sum of all C(n,k) — exactly the elements of row n.
2. Fibonacci numbers appear in shallow diagonals If you draw the triangle left-justified and sum along diagonal bands running upper-right to lower-left, the sums are 1, 1, 2, 3, 5, 8, 13, ... — the Fibonacci sequence. This is a non-trivial identity that surprises people every time.
3. The Hockey Stick identity Pick any element on the left edge (a 1), go diagonally down-right for any number of steps, then turn and go one step down-left. The last element equals the sum of all elements you visited on the diagonal. This is used to derive closed-form formulas for sums like 1 + 2 + 3 + ... + n = C(n+1, 2).
4. Powers of 11 Row 0 = 1 = 11^0. Row 1 = 11 = 11^1. Row 2 = 121 = 11^2. Row 3 = 1331 = 11^3. Row 4 = 14641 = 11^4. It breaks down for row 5 because of carrying, but the pattern holds for the first five rows.
5. Symmetry Each row is a palindrome: C(n, k) = C(n, n-k). This is because choosing k items to include is equivalent to choosing n-k items to exclude. You can exploit this symmetry in LC 119 to halve your computation.
Pascal's Triangle II — Single Row in O(k) Space
LeetCode 119 asks for only the rowIndex-th row (zero-indexed) using at most O(rowIndex) extra space. There are two clean approaches:
Approach 1: In-Place Right-to-Left Update
Maintain a single array of length rowIndex + 1, initialized to all 1s. For each row from 2 to rowIndex, iterate the array from right to left and add arr[j] += arr[j-1]. Going right-to-left ensures you read the old value of arr[j-1] before overwriting it.
def getRow(rowIndex):
row = [1] * (rowIndex + 1)
for i in range(2, rowIndex + 1):
for j in range(i - 1, 0, -1): # right to left
row[j] += row[j - 1]
return row
Approach 2: Direct Formula — O(rowIndex) time, O(rowIndex) space
Use the recurrence between consecutive binomial coefficients:
C(n, k) = C(n, k-1) × (n - k + 1) / k
Start with C(n, 0) = 1 and compute each subsequent element in one pass:
def getRow(rowIndex):
row = [1] * (rowIndex + 1)
for k in range(1, rowIndex + 1):
# C(rowIndex, k) = C(rowIndex, k-1) * (rowIndex - k + 1) / k
row[k] = row[k - 1] * (rowIndex - k + 1) // k
return row
This computes the entire row in a single loop with no inner loop at all — elegant and fast. The key insight is that the ratio between adjacent binomial coefficients is always a rational number that simplifies to an integer at each step.
Visual Dry Run
Let's trace numRows = 5 step by step:
i=0: row = [1]
result = [[1]]
i=1: row = [1, 1] (no interior positions — loop doesn't run)
result = [[1], [1,1]]
i=2: row starts as [1, 1, 1]
j=1: row[1] = result[1][0] + result[1][1] = 1 + 1 = 2
row = [1, 2, 1]
result = [[1],[1,1],[1,2,1]]
i=3: row starts as [1, 1, 1, 1]
j=1: row[1] = result[2][0] + result[2][1] = 1 + 2 = 3
j=2: row[2] = result[2][1] + result[2][2] = 2 + 1 = 3
row = [1, 3, 3, 1]
result = [[1],[1,1],[1,2,1],[1,3,3,1]]
i=4: row starts as [1, 1, 1, 1, 1]
j=1: row[1] = 1 + 3 = 4
j=2: row[2] = 3 + 3 = 6
j=3: row[3] = 3 + 1 = 4
row = [1, 4, 6, 4, 1]
result = [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
Notice how row 4 contains C(4,0)=1, C(4,1)=4, C(4,2)=6, C(4,3)=4, C(4,4)=1 — verifying the binomial coefficient connection directly.
Common Mistakes
Mistake 1: Wrong loop range for interior elements
The most common bug is using range(0, i+1) for the interior loop instead of range(1, i). If you update row[0] or row[i], you overwrite the 1s that were supposed to stay fixed as boundary conditions.
# WRONG — overwrites boundary elements
for j in range(0, i + 1):
row[j] = triangle[i-1][j-1] + triangle[i-1][j] # index out of range at j=0
# CORRECT — only touches interior positions
for j in range(1, i):
row[j] = triangle[i-1][j-1] + triangle[i-1][j]
Mistake 2: Off-by-one in row count
LeetCode says "generate the first numRows rows." The loop should run from i = 0 to i = numRows - 1 (i.e., range(numRows)), giving exactly numRows rows. Using range(1, numRows + 1) or range(numRows + 1) produces the wrong number of rows.
Mistake 3: Not initializing rows correctly before the inner loop
If you initialize row = [0] * (i + 1) and rely entirely on the inner loop to set all values, you must handle positions 0 and i explicitly. Forgetting to do so leaves the boundary elements as 0 instead of 1. The cleaner approach is to initialize row = [1] * (i + 1) and let the inner loop only touch interior positions.
Solutions
LeetCode 118 — Generate Pascal's Triangle
Python
def generate(numRows: int) -> list[list[int]]:
triangle = []
for i in range(numRows):
# Initialize the entire row to 1 — handles edges automatically
row = [1] * (i + 1)
# Only update interior positions; edges stay 1
for j in range(1, i):
# Each interior element is the sum of the two elements above
row[j] = triangle[i - 1][j - 1] + triangle[i - 1][j]
triangle.append(row)
return triangle
JavaScript
/**
* @param {number} numRows
* @return {number[][]}
*/
function generate(numRows) {
const triangle = [];
for (let i = 0; i < numRows; i++) {
// Fill entire row with 1s — first and last are correct already
const row = new Array(i + 1).fill(1);
// Compute interior positions using the previous row
for (let j = 1; j < i; j++) {
row[j] = triangle[i - 1][j - 1] + triangle[i - 1][j];
}
triangle.push(row);
}
return triangle;
}
LeetCode 119 — Return Only the k-th Row
Python — In-Place O(rowIndex) Space
def getRow(rowIndex: int) -> list[int]:
# Single array, updated in place row by row
row = [1] * (rowIndex + 1)
# Start from row 2 (rows 0 and 1 are all 1s by initialization)
for i in range(2, rowIndex + 1):
# Traverse right to left so we read old values before overwriting
for j in range(i - 1, 0, -1):
row[j] += row[j - 1]
return row
Python — Formula Approach (single pass)
def getRow(rowIndex: int) -> list[int]:
row = [1] * (rowIndex + 1)
# C(rowIndex, k) = C(rowIndex, k-1) * (rowIndex - k + 1) / k
for k in range(1, rowIndex + 1):
row[k] = row[k - 1] * (rowIndex - k + 1) // k
return row
JavaScript — Formula Approach
/**
* @param {number} rowIndex
* @return {number[]}
*/
function getRow(rowIndex) {
const row = new Array(rowIndex + 1).fill(1);
// Build using the ratio between adjacent binomial coefficients
// C(n, k) = C(n, k-1) * (n - k + 1) / k
for (let k = 1; k <= rowIndex; k++) {
row[k] = Math.round(row[k - 1] * (rowIndex - k + 1) / k);
}
return row;
}
Complexity Analysis
LC 118 — Generate Full Triangle
| Time | Space | |
|---|---|---|
| Building the triangle | O(numRows²) | O(numRows²) |
The outer loop runs numRows times. Row i has i + 1 elements, so total work is 1 + 2 + 3 + ... + numRows = numRows × (numRows + 1) / 2 = O(numRows²). Space is the same because we store every element.
LC 119 — Single Row
| Approach | Time | Space |
|---|---|---|
| In-place row simulation | O(rowIndex²) | O(rowIndex) |
| Formula (single pass) | O(rowIndex) | O(rowIndex) |
The formula approach is strictly better — it computes each element in O(1) with a single multiplication and division, requiring no inner loop at all.
Follow-Up Questions
LeetCode 62 — Unique Paths
A robot moves from the top-left to the bottom-right of an m × n grid, moving only right or down. The number of unique paths is C(m + n - 2, m - 1) — a single entry in Pascal's Triangle. The DP table for this problem is structurally identical to Pascal's Triangle: dp[i][j] = dp[i-1][j] + dp[i][j-1], with the boundary condition that the top row and left column are all 1.
LeetCode 63 — Unique Paths II
Same structure as LC 62, but some cells are blocked. You set dp[i][j] = 0 when a cell is an obstacle, and apply the same recurrence elsewhere. The Pascal's Triangle intuition — build from boundaries, combine adjacent cells — carries directly.
Triangle DP Pattern (LeetCode 120 — Triangle)
Given a triangle array, find the minimum path sum from top to bottom. The recurrence is dp[i][j] = triangle[i][j] + min(dp[i-1][j-1], dp[i-1][j]) — the same row-by-row build, just with a different operation (min instead of sum). Recognizing the Pascal's Triangle shape in this problem is the key insight.
Coin Change Patterns
The unbounded knapsack / Coin Change family uses a similar "build up table row by row" pattern. Once you are comfortable with Pascal's Triangle, the leap to dp[i][j] = dp[i-1][j] + dp[i][j - coins[i]] feels natural rather than mysterious.
This Pattern Solves
Any problem where you fill a 2D DP table (or a triangular structure) row by row, where each cell depends only on the current or previous row:
- Grid path counting problems (LC 62, 63)
- Triangle minimum path sum (LC 120)
- Problems asking for binomial coefficients explicitly
- Probability distributions on a grid (random walk problems)
- Generating combinations
C(n, k)for largenwithout overflow (use the formula approach from LC 119)
Key Takeaway
Pascal's Triangle teaches the most important habit in dynamic programming: trust the recurrence. Once you see that every interior element is the sum of its two parents, the code writes itself — initialize boundaries to 1, loop over interior positions, read from the previous row, done. The deeper lesson is that this triangle is not just a coding exercise but a window into combinatorics: every element triangle[n][k] answers the question "in how many ways can I choose k items from n?" That connection recurs throughout algorithm problems — Unique Paths, coin combinations, probability trees — so internalizing it here pays dividends across your entire interview preparation.
Advertisement