The Art of Disarray: A Deep Dive into Derangements (Subfactorials)

Правка en1, от ZodiacZ408, 2026-07-01 15:28:33

Hello Codeforces!

Combinatorics and counting problems are a staple in competitive programming. Today, I want to discuss a classic yet incredibly powerful concept that frequently appears in $$$O(N)$$$ DP optimizations and math-heavy problems: Derangements.

Whether you are grinding Div. 2 B/C problems or tackling high-level combinatorics on USACO and AtCoder, understanding the intuition behind derangements is a massive advantage. Let's dive in!


1. What is a Derangement?

A derangement is a permutation of the elements of a set such that no element appears in its original position.

If we have an array $$$A = [1, 2, 3, \dots, N]$$$, a derangement is a permutation $$$P$$$ where $$$P[i] \neq i$$$ for all $$$1 \le i \le N$$$.

Classic Examples: * Secret Santa: $$$N$$$ people put their names in a hat and each draws one. A valid state is when nobody draws their own name. * The Hat-Check Problem: $$$N$$$ guests check their hats. The attendant hands them back randomly. What is the probability nobody gets their own hat?

The number of derangements of a set of size $$$N$$$ is denoted as $$$!N$$$ (read as subfactorial $$$N$$$) or $$$D_n$$$.


2. The Core Mathematics & Recurrences

To solve this efficiently, we don't generate permutations. Instead, we rely on recurrence relations.

Recurrence 1: The Intuitive Approach

The most standard recurrence for derangements is:

$$$D_n = (n - 1)(D_{n-1} + D_{n-2})$$$

Proof / Intuition: Imagine $$$N$$$ people. Let person $$$1$$$ take the spot of person $$$i$$$ ($$$2 \le i \le n$$$). There are exactly $$$(n - 1)$$$ choices for $$$i$$$. Now, what does person $$$i$$$ do? 1. Case 1 (They swap): Person $$$i$$$ takes person $$$1$$$'s original spot. The two people have perfectly swapped. The remaining $$$(n - 2)$$$ people must now be deranged among themselves. This gives $$$D_{n-2}$$$ ways. 2. Case 2 (They don't swap): Person $$$i$$$ does not take person $$$1$$$'s spot. This is mathematically identical to saying person $$$i$$$ is forbidden from exactly one specific spot (spot 1), while the other $$$(n-2)$$$ people are forbidden from their own spots. This maps perfectly to a derangement problem of size $$$(n - 1)$$$. This gives $$$D_{n-1}$$$ ways.

Summing these cases and multiplying by the $$$(n - 1)$$$ choices yields our formula!

Recurrence 2: The Linear Step

A tighter recurrence relation exists, which is incredibly useful for space optimization:

$$$D_n = n \cdot D_{n-1} + (-1)^n$$$

The Closed Form (Inclusion-Exclusion)

Using the Principle of Inclusion-Exclusion (PIE), the exact formula is:

$$$D_n = n! \sum_{i=0}^{n} \frac{(-1)^i}{i!}$$$

(Fun fact: As $$$n \to \infty$$$, the probability of a random permutation being a derangement approaches $$$\frac{1}{e} \approx 36.78\%$$$.)


3. Implementation (C++)

In CP, we usually need to compute $$$D_n \pmod M$$$ (where $$$M = 10^9+7$$$ or $$$998244353$$$).

Approach A: $$$O(N)$$$ Precomputation

Perfect when you need to answer multiple test cases. Space Complexity: $$$O(N)$$$. Time Complexity: $$$O(N)$$$.

#include <iostream>
#include <vector>

using namespace std;

const int MOD = 1e9 + 7;
const int MAXN = 1e6;

vector<long long> D(MAXN + 1);

void precompute() {
    D[0] = 1; // Base case: 1 way to derange 0 elements
    D[1] = 0; // Base case: 0 ways to derange 1 element
    
    for (int i = 2; i <= MAXN; ++i) {
        D[i] = (i - 1) * (D[i - 1] + D[i - 2]) % MOD;
    }
}

void solve() {
    int n;
    cin >> n;
    cout << D[n] << "\n";
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    precompute();
    
    int t;
    cin >> t;
    while (t--) {
        solve();
    }
    return 0;
}

Approach B: $$$O(1)$$$ Space Optimization

If you only need to query $$$D_n$$$ once for a massive $$$N$$$, you can roll the variables just like the Fibonacci sequence. Space Complexity: $$$O(1)$$$.

long long get_derangement(int n) {
    if (n == 0) return 1;
    if (n == 1) return 0;
    
    long long prev2 = 1; // D[0]
    long long prev1 = 0; // D[1]
    long long current = 0;
    
    for (int i = 2; i <= n; ++i) {
        current = (i - 1) * (prev1 + prev2) % MOD;
        prev2 = prev1;
        prev1 = current;
    }
    return current;
}

4. Advanced Variations (For Div 1 / Hard Div 2)

Partial Derangements (Rencontres Numbers): What if a problem asks: "How many permutations exist where exactly $$$K$$$ elements are in their original positions?"

You choose which $$$K$$$ elements stay in place, and then completely derange the remaining $$$N - K$$$ elements:

$$$Answer = \binom{n}{k} \cdot D_{n-k}$$$

Massive $$$N$$$ Queries using NTT: If $$$N \ge 10^5$$$ and the modulus allows, you can view the closed-form PIE formula as a convolution of two polynomials: $$$A[i] = \frac{(-1)^i}{i!}$$$ and $$$B[j] = j!$$$. This allows computing the entire array using Number Theoretic Transform (NTT) in $$$O(N \log N)$$$ time, though standard $$$O(N)$$$ is almost always sufficient unless there's a strict time limit on array generation.


5. Practice Problems


I hope this guide helps solidify your understanding of derangements! Feel free to ask questions in the comments or point out any other interesting problems that use this concept.

Happy coding!

Теги maths, algorithms, combinatorics, tutorial

История

 
 
 
 
Правки
 
 
  Rev. Язык Кто Когда Δ Комментарий
en1 Английский ZodiacZ408 2026-07-01 15:28:33 5628 Initial revision (published)