Structs & Data Structures · intermediate · ~14 min
Counting arrangements recursively.
Counting arrangements is naturally recursive because fixing one position leaves a smaller counting problem. k-permutations P(n,k) chooses an ordered arrangement of k items from n: pick any of the n for the first slot, then arrange k-1 from the remaining n-1, giving P(n,k) = n * P(n-1, k-1). Derangements — permutations where no element stays in its original position — need a subtler two-term recurrence and, crucially, two base cases, because D(n) depends on both D(n-1) and D(n-2).
Permutation counts size the search spaces you are about to explore: knowing that 12! is roughly half a billion tells you immediately whether brute force is viable. Derangements answer the classic "nobody draws their own name" question in secret-santa assignments, and they are a good example of a recurrence whose derivation genuinely matters — you cannot guess it.
k-permutations. P(n,k) = n * P(n-1, k-1), base case P(n,0) = 1 (exactly one way to arrange nothing). Full permutations are the case k = n, giving n!.
Why two base cases for derangements. The recurrence D(n) = (n-1) * (D(n-1) + D(n-2)) reaches back two steps, so a single base case leaves D(1) undefined. Both D(0) = 1 and D(1) = 0 are required — and D(1) = 0 is correct: a single item has nowhere else to go.
Where the recurrence comes from. Place item 1 in any of the n-1 other positions, say position j. Either item j moves to position 1 (leaving a derangement of the remaining n-2), or it does not (leaving a derangement of n-1 with a relabelled constraint). Hence the two terms.
Growth. Factorials explode: 20! is near the limit of a 64-bit integer, and 21! overflows it. Derangements grow at roughly n!/e.
Overlapping subproblems. The naive two-term derangement recursion recomputes values exponentially, exactly like naive Fibonacci — memoise it or compute bottom-up.
/* ordered arrangements of k items from n */
long long pnk(int n, int k) {
return (k == 0) ? 1 : (long long)n * pnk(n - 1, k - 1);
}
/* derangements: TWO base cases are mandatory */
long long derange(int n) {
if (n == 0) return 1; /* the empty arrangement counts */
if (n == 1) return 0; /* one item must stay put */
return (long long)(n - 1) * (derange(n - 1) + derange(n - 2));
}
/* bottom-up avoids the exponential recomputation */
long long derange_fast(int n) {
long long a = 1, b = 0; /* D(0), D(1) */
for (int i = 2; i <= n; i++) { long long c = (long long)(i - 1) * (a + b); a = b; b = c; }
return (n == 0) ? 1 : b;
}
Key points:
long long before multiplying, or the product overflows as int first.derange as written is exponential — the rolling version is the one to use.P(n,0) = 1, not 0; the empty arrangement is a valid arrangement.Counting arrangements is recursive: k-permutations P(n,k) peel one position at a time, and derangements (permutations with no fixed point) follow a two-term recurrence D(n)=(n-1)(D(n-1)+D(n-2)).
#include <stdio.h>
static long long pnk(int n,int k){ return k==0 ? 1 : (long long)n*pnk(n-1,k-1); }
static long long derange(int n){ if(n==0) return 1; if(n==1) return 0; return (long long)(n-1)*(derange(n-1)+derange(n-2)); }
int main(void){
printf("P(10,3) ordered picks = %lld\n", pnk(10,3));
printf("derangements of 5 items = %lld\n", derange(5));
return 0;
}
| Step | Line | What happens |
|---|---|---|
| 1 | pnk(5,2) |
5 * pnk(4,1). |
| 2 | pnk(4,1) |
4 * pnk(3,0). |
| 3 | pnk(3,0) |
Base case -> 1. |
| 4 | unwind | 4 * 1 = 4, then 5 * 4 = 20 — the 20 ordered pairs from 5 items. |
| 5 | derange(4) |
3 * (derange(3) + derange(2)) = 3 * (2 + 1) = 9. |
| 6 | check | The derangement sequence is 1, 0, 1, 2, 9, 44 — matching D(4) = 9. |
Confusing permutations with combinations; a single base case for derangements.
Compiler errors and warnings:
warning: integer overflow in expression — cast to long long before multiplying, not after.Runtime symptoms:
derange. Only one base case, so derange(1) recurses into derange(-1) and downward forever.derange takes visibly long past n = 30. The naive form is exponential — memoise or use the rolling loop.pnk(n,0) returns 0. The base case must return 1.20! is about 2.4 x 10^18, near the long long limit; 21! overflows.Technique: check both against their known sequences — permutations P(5,2) = 20, derangements 1, 0, 1, 2, 9, 44. A single wrong term identifies which base case is off.
n.derange(50) naively would never finish. Memoise or iterate.pnk(n, k) with k > n should be 0, and negative arguments are meaningless — validate at the boundary rather than letting the recursion wander.Concrete uses: Permutation counts size search spaces in scheduling, routing and puzzle solving — they tell you when brute force is hopeless. Derangements model secret-santa draws where nobody may draw themselves, hat-check problems, and randomised assignment with exclusion constraints. Both appear constantly in probability calculations.
Professional best practices:
Beginner:
k steps needs k seeds.Intermediate:
1. (Beginner) k-permutations. Implement long long pnk(int n, int k). Example: P(5,2) -> 20; P(5,0) -> 1. Concepts: base case, cast-before-multiply.
2. (Beginner) Derangements. Implement derange(n) with both base cases. Example: D(4) -> 9. Concepts: two-term recurrence, two seeds.
3. (Intermediate) Make it fast. Rewrite derange bottom-up with two rolling variables and compare timings for n = 40. Concepts: exponential vs linear.
4. (Intermediate) Probability check. Compute D(n)/n! for n = 5..15 and observe it converging to about 0.3679 (1/e). Concepts: validating a recurrence against a known limit.
Fixing one position turns an arrangement count into a smaller one: P(n,k) = n * P(n-1,k-1) with P(n,0) = 1. Derangements — permutations leaving nothing in place — follow D(n) = (n-1)(D(n-1) + D(n-2)), and because that reaches back two steps it needs two base cases, D(0) = 1 and D(1) = 0; supplying only one causes unbounded recursion. The naive two-term form recomputes exponentially, so use a rolling loop. Cast to a wide type before multiplying, since factorial-scale values overflow almost immediately.