Ways to Partition a Set
Given a set of n distinct elements, count the number of ways to split it into exactly k non-empty, unordered subsets, known as the Stirling number of the second kind.
Do this lesson first: climbing stairsExample input
n = 8 elements, k = 3 subsets, so the question is S(8, 3)
Expected output
966
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, as on binomial-coefficient's triangle, and one of the two holds a DIFFERENT NUMBER here. That difference is worth stopping on rather than skimming. dp[0][0] = 1. There is exactly one way to split the empty set into zero subsets: take no subsets at all. It is 1 rather than 0 because an empty collection of subsets is a legitimate arrangement of nothing, and it is the seed the whole triangle is built from. dp[n][0] = 0 for every row past the first, and binomial-coefficient's matching edge is 1. BOTH ARE RIGHT, and the reason is that the two cells count different things. There, the cell counts selections, and choosing NOTHING out of n items is a real selection, the empty one, which always exists and there is exactly one of it. Here the cell counts partitions of a non-empty set into zero non-empty subsets, and zero subsets hold no elements between them, so there is nowhere for the n elements to go and no such arrangement exists at all. One way to choose nothing from anything; no way to split something into nothing. That 0 is not decorative either. Every computed cell in column 1 reads it, which is what pins the whole of column 1 to 1 all the way down: dp[n][1] = 1 × dp[n - 1][1] + 0. Copy that edge across as a 1, which is the likeliest slip for a reader arriving from the other page, and column 3 comes out 7, 32, 122, 423, 1389 across n = 4 to 8 in place of the true 6, 25, 90, 301, 966. dp[n][n] = 1 for every row: there is exactly one way to split n elements into n non-empty subsets, every element alone in its own. Both edges have to be GIVEN rather than summed, and that reason is arithmetic rather than convention. dp[n][0] would reach for dp[n - 1][-1], column -1, which is off the table altogether, and dp[n][n] would reach for dp[n - 1][n], which sits above the diagonal and does not exist. Every cell with 1 <= k <= n - 1 is computed, and each of those has exactly two live predecessors, never one. At n = 8 the rectangle declares 81 cells, 45 of them live: 17 given, one per row down the left edge and one per row past the first on the diagonal, and 28 summed. THE DOTS ARE NOT ZEROS, in the sense that nothing is ever written to them and no cell ever reads one. A position with column > row would be asking to fill more non-empty subsets than there are elements to fill them, which is not a hard question, it is not a question. Nothing could read one by accident either: a computed cell at (n, k) has k <= n - 1, so the two cells it reads, (n - 1, k) and (n - 1, k - 1), both have column at most n - 1 and therefore sit on or below row n - 1's own diagonal. Zero is even the mathematically correct value for those positions, since there are no such partitions, so unlike min-sum-in-a-triangle nothing here would be corrupted by a stray zero above the diagonal. They are dots because they are not part of the question. 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 waysToPartitionASet(n, k) { - 2
const dp = Array.from({ length: n + 1 }, () => new Array(n + 1).fill(0)); - 3
dp[0][0] = 1; - 4
for (let i = 1; i <= n; i++) { - 5
dp[i][0] = 0; - 6
dp[i][i] = 1; - 7
for (let j = 1; j < i; j++) { - 8
const joinExisting = j * dp[i - 1][j]; - 9
const startNew = dp[i - 1][j - 1]; - 10
dp[i][j] = joinExisting + startNew; - 11
} - 12
} - 13
return dp[n][k]; - 14
}
The code, the trap, the variations
- 1
function waysToPartitionASet(n, k) { - 2
const dp = Array.from({ length: n + 1 }, () => new Array(n + 1).fill(0)); - 3
dp[0][0] = 1; - 4
for (let i = 1; i <= n; i++) { - 5
dp[i][0] = 0; - 6
dp[i][i] = 1; - 7
for (let j = 1; j < i; j++) { - 8
const joinExisting = j * dp[i - 1][j]; - 9
const startNew = dp[i - 1][j - 1]; - 10
dp[i][j] = joinExisting + startNew; - 11
} - 12
} - 13
return dp[n][k]; - 14
}
Where people go wrong
Dropping the coefficient. Write dp[n][k] = dp[n - 1][k] + dp[n - 1][k - 1], which is binomial-coefficient's recurrence, and everything about the picture stays right: same triangle, same two arrows, same bases, nothing thrown, whole numbers throughout. What you get is Pascal's triangle SHIFTED one row down and one column right, not Pascal's triangle in place, because this page's left edge is 0 where Pascal's is 1. Measured, the broken table holds exactly C(n - 1, k - 1) at every cell with 1 <= k <= n <= 10. Three things let it survive a glance, and the numbers are what make the point. FIRST, it is not wrong everywhere. Of the 45 live cells at n = 8 it gets 24 right, and those 24 are precisely column 0, column 1 and the diagonal, the cells whose value is forced to a 0 or a 1 anyway. All 21 cells with 2 <= k <= n - 1 are wrong. The first cell it gets wrong is dp[3][2], where it says 2 and the truth is 3, so it has already failed AT n = 3, not somewhere past it. SECOND, the wrong answers are a recognisable sequence rather than obvious garbage: column 3 comes out 3, 6, 10, 15, 21 across n = 4 to 8, which are the triangular numbers, against the true 6, 25, 90, 301, 966. THIRD, and worst, one of the wrong answers is a right answer from next door. The broken table returns 6 at n = 5, and 6 is the correct value of S(4, 3), so a spot check at a single size can land on a number it has seen before and be reassured by it. The other two mistakes are about which index goes where, and both are worth measuring rather than waving at. Put the coefficient on the OTHER term, dp[n][k] = k * dp[n - 1][k - 1] + dp[n - 1][k], and column 3 comes out 10, 25, 46, 73, 106 across n = 4 to 8: wrong at four of the five sizes and exactly right at n = 5, where it returns 25. That is why the independent check behind this page runs every size the slider offers rather than one. Use the ROW index as the multiplier instead of the column and it is wrong everywhere and wildly so, 9, 71, 580, 5104, 48860 over the same range. And reading the two axes the wrong way round puts you in the empty half: S(8, 3) is dp[8][3], the 966, while dp[3][8] is a dot, because splitting 3 elements into 8 non-empty subsets is not a small number, it is not a thing at all.
Ask for the number of ways to split the n elements into ANY number of non-empty subsets rather than exactly k.
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 SUM of the bottom row. Those totals are the Bell numbers, measured as 1, 1, 2, 5, 15, 52, 203, 877, 4140 for rows 0 through 8, and the sweep was already computing every term of the sum. That is the generality argument made concrete, and it is the same relationship nth-row-of-pascals-triangle has with binomial-coefficient: one cell against a whole row of the same table. It also keeps the rolling-row space reduction alive, since the final row is exactly what the rolling row holds, but it kills the option of stopping the sweep at column k, because now every column of the bottom row is part of the answer.
Return S(n, k) modulo a prime p, the usual competitive-programming form.
The recurrence does not change: still the column index times the cell above, plus the cell up and to the left. 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. This is also the variation that makes the table clearly worth having over the closed form, which divides by k! at the end: dividing modulo p means multiplying by the modular inverse of k!, real extra machinery to get right, while the additive table needs a multiply, a plus and a percent.
Count distributions into k LABELLED boxes with none left empty, which is the number of surjections from an n-element set onto a k-element set.
Naming the subsets turns each partition into k! distinct distributions, so the answer is k! * S(n, k), which is 36, 150, 540, 1806, 5796 across n = 4 to 8 against this page's 6, 25, 90, 301, 966. The interesting part is that the labelled table has a recurrence of its own and it is a different SHAPE of arithmetic: T(n, k) = k * (T(n - 1, k) + T(n - 1, k - 1)), one factor over a bracketed sum, verified against k! * S(n, k) for every k up to 6 and n up to 10. That is the form painting-fence and count-derangements narrate, and it is NOT the same quantity as k * T(n - 1, k) + T(n - 1, k - 1); the same triangle and the same two arrows admit both, which is why a page has to be explicit about where the bracket goes. The shape and fill order are unchanged and the left edge stays 0, but the diagonal moves: T(n, n) = n!, so it runs 1, 1, 2, 6, 24, 120, 720, 5040, 40320 instead of all 1s. One more thing falls out of it. The 5796 at n = 8 is exactly the alternating sum the check behind this page forms before dividing by 3!, and that is not a coincidence: inclusion and exclusion counts the LABELLED distributions, and the division by k! is precisely what turns labelled boxes back into unordered subsets.