Binomial Coefficient
Given integers n and r, compute the number of ways to choose r items out of a set of n distinct items, without regard to the order the items are chosen in.
Do this lesson first: climbing stairsExample input
n = 6, r = 3, so the question is C(6, 3)
Expected output
20
Break it down
Answer each question out loud before you open it. Getting it wrong here is the useful part. A revealed answer you never guessed at teaches you nothing.
Fill the table
The table pauses before each cell you have to supply. Type the value the recurrence gives, and the write animation confirms it.
Press start. The animation stops at every cell YOUR recurrence must fill.
BOTH outer edges are given, and the reason is arithmetic rather than convention. dp[n][0] = 1 for every row: there is exactly one way to choose nothing, the empty selection, and it always exists. It has to be a base because the cell the recurrence would send it to is dp[n - 1][-1], column -1, which is off the table altogether. dp[n][n] = 1 for every row: there is exactly one way to choose all n items, take every one of them. It has to be a base because the cell the recurrence would send it to is dp[n - 1][n], which sits above the diagonal and does not exist. Every cell with 1 <= r <= n - 1 is computed, and each of those has exactly two live predecessors, never one. THE DOTS ARE NOT ZEROS, in the sense that nothing is ever written to them and no cell ever reads one: a grid has to be a rectangle, so the table is declared n + 1 by n + 1, but only the cells with column <= row belong to the triangle, and a position with column > row would be asking for more items than the row has to offer. Here that matters less than it does in min-sum-in-a-triangle, and the difference is worth being precise about rather than borrowing that page's alarm. There, a zero above the diagonal would be a free number a descent could cheat through, and the answer would come out below the truth. The two pages depend on the same two directions, up-and-to-the-left and straight up, so that is not the difference. The difference is that this page makes the DIAGONAL a base while the triangle computes it: a computed cell here reads dp[n - 1][r - 1] and dp[n - 1][r], and since r <= n - 1 both of those are on or below the diagonal, so the empty half is unreachable from any live cell. Zero is even the mathematically correct value for those positions, since there are no ways to choose more items than exist. The triangle has no such guarantee, because its diagonal cells are computed rather than given, and the straight-up read from one of those lands in the row above, which is a column shorter; that is the read its spec has to leave out by hand. They are dots because they are not part of the question, not because a zero there would corrupt anything. Filling row-major, left to right within each row, means both cells a computed cell reads are final before it reads them, which is the only ordering property the recurrence needs.
- 1
function binomialCoefficient(n, r) { - 2
const dp = Array.from({ length: n + 1 }, () => new Array(n + 1).fill(0)); - 3
for (let i = 0; i <= n; i++) { - 4
dp[i][0] = 1; - 5
dp[i][i] = 1; - 6
for (let j = 1; j < i; j++) { - 7
const takeIt = dp[i - 1][j - 1]; - 8
const leaveIt = dp[i - 1][j]; - 9
dp[i][j] = takeIt + leaveIt; - 10
} - 11
} - 12
return dp[n][r]; - 13
}
The code, the trap, the variations
- 1
function binomialCoefficient(n, r) { - 2
const dp = Array.from({ length: n + 1 }, () => new Array(n + 1).fill(0)); - 3
for (let i = 0; i <= n; i++) { - 4
dp[i][0] = 1; - 5
dp[i][i] = 1; - 6
for (let j = 1; j < i; j++) { - 7
const takeIt = dp[i - 1][j - 1]; - 8
const leaveIt = dp[i - 1][j]; - 9
dp[i][j] = takeIt + leaveIt; - 10
} - 11
} - 12
return dp[n][r]; - 13
}
Where people go wrong
Computing it as n! / (r! (n - r)!). The formula is correct on paper and wrong in a fixed-width number type, and it fails far earlier than anyone expects: 19! is already past the largest integer a double represents exactly, so in JavaScript that route hands back 253.00000000000003 for C(23, 2), whose true value is the small integer 253, and by C(171, 2) the numerator has overflowed to Infinity while the answer itself is only 14535. The additive table never divides and never leaves the integers, which is the whole reason to prefer it at these sizes. The second real mistake is reading the two axes the wrong way round: C(6, 3) is dp[6][3], the 20, while dp[3][6] is a dot, because choosing 6 items out of 3 is not a small number, it is not a thing at all. If a lookup lands above the diagonal, the arguments went in swapped.
Return C(n, r) modulo a prime p, the usual competitive-programming form.
The recurrence does not change at all: still the sum of the two cells above. Only the write changes, taking a % p after the addition, which keeps every cell below p and removes the overflow question entirely. Shape, bases, fill order and dependency arrows are all untouched, which is why this variation is a one-token edit rather than a redesign.
Ask for an entire row of the triangle rather than one entry.
Nothing about the recurrence, the bases or the fill order moves; only what you read at the end does, switching from one highlighted cell to the whole bottom row. That is the generality argument made concrete: the sweep was already computing every entry of that row, so the extra answers cost nothing. It is also the reason the O(n) rolling-row space trick survives here, since the final row is exactly what the rolling row holds.
Count partitions of n labelled items into exactly r non-empty unordered groups, the Stirling numbers of the second kind.
The two predecessors stay the same two cells above and the triangular shape stays identical, so the picture on this page is unchanged. What changes is that the second term picks up the column as a multiplier, dp[n][r] = dp[n - 1][r - 1] + r * dp[n - 1][r], because an item left out of the singled-out group can join any one of the r groups already there rather than just one. The diagonal is still 1, but the left edge flips to 0 for every row past the first: there is no way to split a non-empty set into zero groups. Same arrows, different arithmetic on them, and a much faster-growing table.