April 18, 2026 8 min read 8 views

Dynamic Programming in the Hat Check Puzzle

Master the Hat Check Problem: Discover the math and Python code behind derangements. Learn dynamic programming and O(1) space optimization to ace your next technical interview.

Problem Introduction

Imagine this: You and three friends attend a fancy party. Everyone checks their hats with an attendant. As the party ends, you all rush to leave. The attendant, having lost all the claim tickets, randomly returns hats to the four of you.

What is the probability that absolutely no one receives their own hat? What if there were 10 people? 100 people? This is the famous hat check problem (also known as the problème des rencontres).

The puzzle has surprised mathematicians for centuries because the probability converges to a specific constant—approximately 36.8%—regardless of how many people attend.

Why It Matters

The hat check problem appears in:

  • Quantitative finance interviews: Hedge funds use this to test probability reasoning

  • Cryptography: Understanding derangements helps analyze permutation-based ciphers

  • Randomized algorithm testing: Verifying that shuffling algorithms produce uniformly random permutations

  • Looking to step up your coding interview prep? Check out our Tutoring Page for personalized 1-on-1 coaching.

Step-by-Step Breakdown

Step 1: Understanding the Problem as a Probability

Let H(N) be the number of ways to return hats so no one gets their own hat. The total number of ways to return hats (without restrictions) is N! (N factorial).

Therefore, the probability P(N) that no one gets their own hat is:

Plain Text

P(N) = H(N) / N!

 

Our job is to compute H(N) efficiently, then calculate the probability.

Step 2: Visualizing with a Small Example

Visualizing with a Small Example

This diagram shows the branching nature. Hat 1 can go to any of the other 3 people. The path then branches based on where Hat 2 goes.

Step 3: Establishing Base Cases

Let's manually compute small values to build intuition:

N = 1: One person, one hat. The only possible return gives the person their own hat. No valid returns. H(1) = 0. Probability = 0/1 = 0.

N = 2: Two people (A and B), two hats (a and b). Valid returns: A gets b, B gets a. One valid arrangement. H(2) = 1. Probability = 1/2 = 0.5.

N = 3: List all 6 permutations of [A,B,C] with hats [a,b,c]:

  • [a,b,c] → all correct (invalid)

  • [a,c,b] → A correct (invalid)

  • [b,a,c] → C correct (invalid)

  • [b,c,a] → no one correct (valid)

  • [c,a,b] → no one correct (valid)

  • [c,b,a] → B correct (invalid)

H(3) = 2. Probability = 2/6 ≈ 0.3333.

N = 4: Without listing all 24 permutations, we can compute H(4) = 9. Probability = 9/24 = 0.375.

Notice the probabilities: 0, 0.5, 0.333, 0.375. They seem to converge around 0.3679.

Step 4: Deriving the Recurrence Relation

Let H(N) be the number of hat returns with no fixed points.

Step 1: Where does Hat 1 go? Hat 1 belongs to Person 1. It cannot go to Person 1. So it goes to Person k, where k ∈ {2, 3, ..., N}. That's (N - 1) possibilities.

Step 2: Where does Hat k go? Now consider Hat k (the hat belonging to Person k). Two scenarios:

Scenario A: Hat k goes to Person 1

  • Hat 1 is with Person k, Hat k is with Person 1

  • These two hats have been swapped

  • The remaining (N - 2) hats must be returned to their owners with no one getting their own hat

  • Number of ways: H(N - 2)

Scenario B: Hat k does NOT go to Person 1

  • Hat 1 is with Person k

  • Hat k must go to someone else (not Person 1)

  • This effectively creates a derangement problem for the remaining (N - 1) hats, where Hat k cannot go to Person 1

  • Number of ways: H(N - 1)

Step 3: Combine
For each of the (N - 1) choices for where Hat 1 goes, we have H(N - 1) + H(N - 2) arrangements for the remaining hats.

Python

H(N) = (N - 1) × [H(N - 1) + H(N - 2)]

 

Check against our manual calculations:

  • H(3) = 2 × (H(2) + H(1)) = 2 × (1 + 0) = 2 ✓

  • H(4) = 3 × (H(3) + H(2)) = 3 × (2 + 1) = 9 ✓

Step 5: Naive Recursive Implementation

Python

def hat_check_recursive(n: int) -> int:
    """
    Naive recursive solution for the hat check problem.
    
    Time: O(2^n) - Exponential
    Space: O(n) - Recursion stack
    
    This works for n <= 30 but becomes impractical beyond that.
    """
    if n == 1:
        return 0
    if n == 2:
        return 1
    
    return (n - 1) * (hat_check_recursive(n - 1) + 
                      hat_check_recursive(n - 2))

def hat_check_probability_recursive(n: int) -> float:
    """Calculate probability using the recursive method."""
    from math import factorial
    return hat_check_recursive(n) / factorial(n)

print(f"P(5) = {hat_check_probability_recursive(5):.6f}")  # 0.366667

 

The problem: The recursive tree for H(40) would have approximately 2^40 ≈ 1 trillion nodes. Impossible.

Step 6: Top-Down DP with Memoization

Python

def hat_check_top_down(n: int, memo: dict = None) -> int:
    """
    Top-down dynamic programming with memoization.
    
    Time: O(n)
    Space: O(n) - Memo dictionary + recursion stack
    """
    if memo is None:
        memo = {}
    
    if n == 1:
        return 0
    if n == 2:
        return 1
    if n in memo:
        return memo[n]
    
    memo[n] = (n - 1) * (hat_check_top_down(n - 1, memo) + 
                         hat_check_top_down(n - 2, memo))
    return memo[n]

def analyze_hat_check(n: int) -> None:
    """Analyze the hat check problem for n people."""
    arrangements = hat_check_top_down(n)
    total = math.factorial(n)
    probability = arrangements / total
    
    print(f"N = {n}")
    print(f"Valid arrangements: {arrangements:,}")
    print(f"Total arrangements: {total:,}")
    print(f"Probability: {probability:.6f}")
    print(f"1/e: {1/math.e:.6f}")
    print("---")

# Example
analyze_hat_check(10)
# Output: Valid arrangements: 1,334,961
# Probability: 0.367879
# 1/e: 0.367879

 

Observation: By N = 10, the probability matches 1/e to 6 decimal places.

Step 7: Bottom-Up DP with Tabulation

Python

def hat_check_bottom_up(n: int) -> int:
    """
    Bottom-up dynamic programming using tabulation.
    
    Time: O(n)
    Space: O(n) - Full DP array
    
    This approach is more efficient than top-down for very large n
    because it avoids recursion overhead.
    """
    if n == 1:
        return 0
    if n == 2:
        return 1
    
    dp = [0] * (n + 1)
    dp[1] = 0
    dp[2] = 1
    
    for i in range(3, n + 1):
        dp[i] = (i - 1) * (dp[i - 1] + dp[i - 2])
    
    return dp[n]

 

Why tabulation is useful: The iterative structure allows us to easily observe the convergence of H(N)/N! to 1/e. We can also compute probabilities for much larger N without recursion limits.

Step 8: Space-Optimized Solution

Python

def hat_check_optimized(n: int) -> int:
    """
    Space-optimized bottom-up solution.
    
    Time: O(n)
    Space: O(1) - Only two variables
    
    This is the optimal solution for exact counting.
    """
    if n == 1:
        return 0
    if n == 2:
        return 1
    
    prev2 = 0  # H(1)
    prev1 = 1  # H(2)
    
    for i in range(3, n + 1):
        current = (i - 1) * (prev1 + prev2)
        prev2 = prev1
        prev1 = current
    
    return prev1

def hat_check_probability_optimized(n: int) -> float:
    """Calculate probability using optimized method."""
    from math import factorial
    return hat_check_optimized(n) / factorial(n)

# Demonstrate convergence
for n in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20]:
    prob = hat_check_probability_optimized(n)
    print(f"N={n:2d}: P = {prob:.8f} | Difference from 1/e = {abs(prob - 1/math.e):.8f}")

 

Output:

Plain Text

N= 1: P = 0.00000000 | Difference = 0.36787944
N= 2: P = 0.50000000 | Difference = 0.13212056
N= 3: P = 0.33333333 | Difference = 0.03454611
N= 4: P = 0.37500000 | Difference = 0.00712056
N= 5: P = 0.36666667 | Difference = 0.00121277
N= 6: P = 0.36805556 | Difference = 0.00017612
N= 7: P = 0.36785714 | Difference = 0.00002230
N= 8: P = 0.36788194 | Difference = 0.00000250
N= 9: P = 0.36787919 | Difference = 0.00000025
N=10: P = 0.36787946 | Difference = 0.00000002

 

Ready to crush your next technical interview?
Visit our Order Page now to grab our exclusive Dynamic Programming bootcamp courses!


Common Mistakes

Mistake 1: Treating the Hat Check as a Standard Probability Problem

Many beginners assume all permutations are equally likely to be derangements. While true, they then incorrectly compute probabilities using approximations without understanding the exact count.

Mistake 2: Forgetting That H(1) = 0

A surprising number of people think H(1) = 1. But with one person and one hat, the only possible return gives the person their own hat. That's not a valid derangement.

Mistake 3: Assuming Independence

Some try to compute the probability as product of individual probabilities: (N-1)/N × (N-2)/(N-1) × ... This gives 1/N, which is wrong. Events are not independent.

Mistake 4: Using Floating-Point for Large N

When computing H(N) / N! for large N, both numbers become astronomically large. H(100) has about 158 digits. Use Python's decimal module or compute the probability via the inclusion-exclusion formula:

Python

P(N) = Σ_{k=0 to N} (-1)^k / k!

 

Frequently Asked Questions

Q1: Does the hat check probability always approach 1/e from alternating sides?

A: Yes. For odd N, the probability is slightly below 1/e. For even N, slightly above 1/e. This alternating convergence is a property of the inclusion-exclusion series.

Q2: How is the hat check problem used in finance interviews?

A: Interviewers use it to test: (1) ability to set up recurrence relations, (2) understanding of dynamic programming, and (3) knowledge of probability convergence. It's a medium-difficulty problem that separates candidates.

Q3: Can the hat check problem be solved with a closed formula?

A: Yes. The number of derangements is given by: !N = N! × Σ_{k=0 to N} ((-1)^k / k!). This is known as the subfactorial formula.

Q4: Why does the attendant losing tickets make this a derangement problem?

A: Without tickets, the attendant returns hats randomly. Each return permutation is equally likely. We want permutations with zero fixed points—that's a derangement.

Conclusion

The hat check puzzle beautifully illustrates how a simple probability question leads to deep combinatorial mathematics. You've seen how the recurrence H(N) = (N-1) × (H(N-1) + H(N-2)) emerges from logical reasoning about swaps and derangements.

More importantly, you've learned that the probability converges to 1/e ≈ 36.79% for any reasonably large group. This constant appears throughout mathematics—in the hat check problem, in the number of permutations with no fixed points, and in the expected number of fixed points in a random permutation.

The dynamic programming techniques you've practiced here—top-down memoization, bottom-up tabulation, and space optimization—are transferable to countless other problems. 

Keep this hat check solution in your toolkit for your next interview.


Take Your Skills to the Next Level!

Want to master Advanced Dynamic Programming alongside FAANG engineers? Our platform has everything you need.


👉 Visit Our Tutoring Page  to book a session today.


👉 Check Our Order Page  for direct access to our full DP coursework!

 
Explore more articles


 

Tags:

Related Posts

Binary Search Explained: Algorithm, Examples, & Edge Cases

Master the binary search algorithm with clear, step-by-step examples. Learn how to implement efficient searches in sorted arrays, avoid common …

Mar 11, 2026
How to Approach Hard LeetCode Problems | A Strategic Framework

Master the mental framework and strategies to confidently break down and solve even the most challenging LeetCode problems.

Mar 06, 2026
Two Pointer Technique | Master Array Problems in 8 Steps

Master the two-pointer technique to solve complex array and string problems efficiently. This guide breaks down patterns, provides step-by-step examples, …

Mar 11, 2026

Need Coding Help?

Get expert assistance with your programming assignments and projects.