Structs & Data Structures · intermediate · ~14 min
Enumerate outcomes by summing over choices.
Counting recursions enumerate outcomes by summing over every valid first choice. Two representatives here: counting the ways d dice with f faces sum to a target, and counting compositions — the ordered ways to write n as a sum of positive parts. Both share the same skeleton: for each legal first choice, recurse on the remainder and add the results. And both illustrate the same practical point as the memoization lesson — the naive form recomputes heavily, so a cache (or a bottom-up table) is what makes them usable.
"In how many ways can this happen?" is the core question of discrete probability, and these counts feed directly into expected values and distributions. Dice-sum counts give exact probabilities for board games and risk models; compositions count how many ways a quantity can be split into ordered parts, which appears in partitioning, parsing and resource allocation.
Sum over first choices. ways(state) = sum over each legal choice c of ways(state after c). That is the whole pattern; the guards on "legal" are where the correctness lives.
Dice sums. With d dice of f faces, ways(d, t) = sum over face = 1..f of ways(d-1, t-face). Base cases: ways(0, 0) = 1 (no dice, target met) and ways(0, t != 0) = 0. Prune when t < 0.
Compositions. comp(n) = sum over k = 1..n of comp(n-k), with comp(0) = 1. The closed form is 2^(n-1) for n >= 1, which makes it an ideal self-check — if your code disagrees, the recurrence or a base case is wrong.
Base cases are the seeds of every count. comp(0) = 1 is what makes the sum non-zero at all; setting it to 0 collapses every result to 0.
Overlap demands a cache. comp(n) calls comp(n-1) down to comp(0) from many branches. Memoise with a sentinel, or build bottom-up.
Growth and overflow. 2^(n-1) passes 64-bit range around n = 64; dice counts grow quickly too. Use long long and apply any specified modulus.
#include <stdlib.h>
/* compositions of n: memo[] pre-filled with -1 */
long long comp(int n, long long *m) {
if (n == 0) return 1; /* the empty composition */
if (m[n] >= 0) return m[n];
long long t = 0;
for (int k = 1; k <= n; k++) t += comp(n - k, m); /* every legal first part */
return m[n] = t;
}
/* dice sums, bottom-up: dp[s] = ways to reach s with the dice seen so far */
long long dice(int d, int f, int t) {
if (d < 0 || t < 0) return 0;
long long *dp = calloc((size_t)t + 1, sizeof *dp);
if (!dp) return -1;
dp[0] = 1; /* zero dice reach 0 exactly one way */
for (int i = 0; i < d; i++)
for (int s = t; s >= 0; s--) { /* DOWNWARD: one die per pass */
long long acc = 0;
for (int face = 1; face <= f; face++)
if (s - face >= 0) acc += dp[s - face];
dp[s] = acc;
}
long long r = dp[t];
free(dp);
return r;
}
Key points:
comp(0) = 1 seeds everything; without it all counts are 0.long long throughout, and calloc supplies the zeros the accumulation needs.Recursion counts configurations by summing over each first choice: dice sums add over each die face, and compositions add over each first part. Both are counting recurrences (memoized where needed).
#include <stdio.h>
#include <stdlib.h>
static long long dice(int d,int f,int t){ if(d<0||t<0) return 0; long long*dp=calloc(t+1,sizeof(long long)); dp[0]=1; for(int i=1;i<=d;i++){ long long*nd=calloc(t+1,sizeof(long long)); for(int s=0;s<=t;s++) if(dp[s]) for(int v=1;v<=f&&s+v<=t;v++) nd[s+v]+=dp[s]; free(dp); dp=nd; } long long r=dp[t]; free(dp); return r; }
static long long comp(int n,long long*m){ if(n==0) return 1; if(m[n]>=0) return m[n]; long long t=0; for(int k=1;k<=n;k++) t+=comp(n-k,m); return m[n]=t; }
int main(void){
printf("ways for 3 six-sided dice to total 10 = %lld\n", dice(3,6,10));
long long m[9]; for(int i=0;i<9;i++) m[i]=-1;
printf("compositions of 8 = %lld\n", comp(8,m));
return 0;
}
| Step | Line | What happens |
|---|---|---|
| 1 | comp(3) |
Sums comp(2) + comp(1) + comp(0) — first part 1, 2 or 3. |
| 2 | comp(2) |
comp(1) + comp(0) = 1 + 1 = 2. |
| 3 | comp(1) |
comp(0) = 1. |
| 4 | total | 2 + 1 + 1 = 4, matching 2^(3-1). The compositions are 1+1+1, 1+2, 2+1, 3. |
| 5 | cache | On the second reference to comp(1), m[1] >= 0 returns instantly. |
| 6 | dice(2,6,7) |
6 ways — the familiar count of rolling 7 with two dice. |
Exponential recursion without memoization; miscounting the empty/base case.
Compiler errors and warnings:
-Wsign-compare at the calloc size; cast to size_t.int.Runtime symptoms:
comp(0) returns 0 instead of 1, or dp[0] was never set to 1. The seed is what every count grows from.comp(n) does not equal 2^(n-1). The loop bound is wrong — the first part ranges over 1..n inclusive.n = 64 for compositions; use a modulus if the problem specifies one.Technique: compare comp(n) against 2^(n-1) for n = 1..20, and dice(2,6,7) against the known 6. Both are exact oracles that pinpoint a wrong base case immediately.
-1, not 0, since 0 is a legitimate answer for unreachable states.t + 1 entries with t from input: reject negative t before the size_t cast, and check calloc for NULL.calloc is correct for the dice table because entries are accumulated into and must start at zero — the opposite of the memo case, where zero is ambiguous. Knowing which situation you are in is the point.s - face >= 0 must be checked before indexing dp[s - face].Concrete uses: Dice-sum distributions drive probability calculations in games, risk models and simulations. Compositions count ordered partitions — how many ways to split a job into consecutive batches, or a length into ordered segments. The same sum-over-first-choice pattern underlies counting parse trees, tilings, and paths through a DAG.
Professional best practices:
Beginner:
long long from the start.Intermediate:
1. (Beginner) Compositions. Implement comp(n) with memoization and verify comp(n) == 2^(n-1) for n = 1..20. Example: comp(3) -> 4. Concepts: sum over first choices, base case seed.
2. (Beginner) Dice sums. Implement dice(d, f, t). Example: dice(2,6,7) -> 6; dice(2,6,1) -> 0. Concepts: per-die passes, downward sweep.
3. (Intermediate) Brute-force oracle. Enumerate all dice outcomes for d <= 4 and confirm the counts match. Concepts: validating counting code.
4. (Intermediate) Restricted parts. Count compositions using only parts of size 1 or 2, and confirm the result is the Fibonacci sequence. Concepts: restricting the choice set.
Counting recursions sum over every legal first choice, so the base case is the seed that every count grows from — comp(0) = 1 and dp[0] = 1 are what make the totals non-zero at all. Both examples overlap heavily, so a memo (seeded to a sentinel that cannot be a valid count, never 0) or a bottom-up table is what makes them practical. Validate against an exact oracle wherever one exists — compositions are 2^(n-1), two dice make 7 in six ways — because counting bugs are otherwise invisible, and use long long since these totals grow exponentially.