Блог пользователя ZodiacZ408

Автор ZodiacZ408, история, 3 месяца назад, По-английски

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!

  • Проголосовать: нравится
  • +5
  • Проголосовать: не нравится

»
3 месяца назад, скрыть # |
 
Проголосовать: нравится 0 Проголосовать: не нравится

very interesting blog, i remember seeing something like this on a math comp. Hopefully i get to use this in future