220 lines
9.4 KiB
YAML
220 lines
9.4 KiB
YAML
title: Airplane Seat Assignment Probability
|
||
slug: airplane-seat-assignment-probability
|
||
difficulty: medium
|
||
leetcode_id: 1227
|
||
leetcode_url: https://leetcode.com/problems/airplane-seat-assignment-probability/
|
||
categories:
|
||
- math
|
||
- dynamic-programming
|
||
patterns:
|
||
- slug: dynamic-programming
|
||
is_optimal: true
|
||
|
||
function_signature: "def nth_person_gets_nth_seat(n: int) -> float:"
|
||
|
||
test_cases:
|
||
visible:
|
||
- input: { n: 1 }
|
||
expected: 1.0
|
||
- input: { n: 2 }
|
||
expected: 0.5
|
||
hidden:
|
||
- input: { n: 3 }
|
||
expected: 0.5
|
||
- input: { n: 10 }
|
||
expected: 0.5
|
||
- input: { n: 100 }
|
||
expected: 0.5
|
||
- input: { n: 1000 }
|
||
expected: 0.5
|
||
|
||
description: |
|
||
`n` passengers board an airplane with exactly `n` seats. The first passenger has lost their ticket and picks a seat randomly. After that, the rest of the passengers will:
|
||
|
||
- Take their own seat if it is still available, and
|
||
- Pick other seats randomly when they find their seat occupied
|
||
|
||
Return *the probability that the* `n`<sup>th</sup> *person gets their own seat*.
|
||
|
||
constraints: |
|
||
- `1 <= n <= 10^5`
|
||
|
||
examples:
|
||
- input: "n = 1"
|
||
output: "1.00000"
|
||
explanation: "The first person can only get the first seat (which is also their own seat)."
|
||
- input: "n = 2"
|
||
output: "0.50000"
|
||
explanation: "The second person has a probability of 0.5 to get the second seat (when the first person randomly picks seat 1 instead of seat 2)."
|
||
|
||
explanation:
|
||
intuition: |
|
||
This problem appears complex at first — with randomness cascading through passengers, it seems like we'd need to track many probability branches. But there's a beautiful mathematical insight that simplifies everything.
|
||
|
||
**The Key Insight:** Focus on *when the chaos ends*. The randomness only continues until someone sits in either **seat 1** (the first passenger's assigned seat) or **seat n** (the last passenger's seat). Every other seat assignment just passes the problem along.
|
||
|
||
Think of it like this: imagine you're passenger `n` waiting to board. The only outcomes that matter to you are:
|
||
- Someone eventually sits in seat 1 → the chain reaction ends, and your seat remains free
|
||
- Someone eventually sits in seat `n` → you lose your seat
|
||
|
||
Here's the magic: at every step where a passenger must choose randomly, seats 1 and `n` are **equally likely** to be chosen (directly or indirectly). This symmetry means the probability is always **50/50** — regardless of how many passengers there are!
|
||
|
||
The only exception is `n = 1`, where the first passenger *is* the last passenger, so they get their own seat with probability 1.
|
||
|
||
approach: |
|
||
We can solve this with pure **mathematical reasoning**:
|
||
|
||
**Step 1: Understand the recursion**
|
||
|
||
- Let `f(n)` = probability that passenger `n` gets their seat
|
||
- The first passenger picks randomly from `n` seats
|
||
|
||
|
||
|
||
**Step 2: Analyse the three cases when passenger 1 picks**
|
||
|
||
- **Picks seat 1** (probability `1/n`): Everyone else gets their own seat. Passenger `n` gets seat `n`. ✓
|
||
- **Picks seat n** (probability `1/n`): Passenger `n` loses their seat immediately. ✗
|
||
- **Picks seat k** where `1 < k < n` (probability `(n-2)/n`): Passengers 2 through `k-1` get their seats. Passenger `k` faces the same problem with `n-k+1` remaining "uncertain" seats.
|
||
|
||
|
||
|
||
**Step 3: Recognise the symmetry**
|
||
|
||
- In case 3, passenger `k` becomes a "new first passenger" for the subproblem
|
||
- The recursive structure shows that seats 1 and `n` always have equal probability of being taken
|
||
- This gives us: `f(n) = 1/n + (n-2)/n × f(smaller subproblem)`
|
||
|
||
|
||
|
||
**Step 4: Solve the recurrence**
|
||
|
||
- Working through the math (or computing small cases), we find `f(n) = 0.5` for all `n >= 2`
|
||
- For `n = 1`: the only passenger gets their own seat, so `f(1) = 1`
|
||
|
||
|
||
|
||
The solution becomes trivially simple: return `1.0` if `n == 1`, else return `0.5`.
|
||
|
||
common_pitfalls:
|
||
- title: Overcomplicating with Simulation
|
||
description: |
|
||
A natural instinct is to simulate the boarding process with random number generation and run many trials to estimate the probability.
|
||
|
||
While this "Monte Carlo" approach works conceptually, it's:
|
||
- Slow and imprecise (needs millions of trials for accuracy)
|
||
- Unnecessary once you understand the mathematical pattern
|
||
- Missing the elegant insight that makes this problem beautiful
|
||
|
||
The closed-form solution runs in O(1) time and is exact.
|
||
wrong_approach: "Simulate boarding millions of times"
|
||
correct_approach: "Use mathematical insight for O(1) solution"
|
||
|
||
- title: Building a Full DP Table
|
||
description: |
|
||
You might try to build a DP solution computing `f(2)`, `f(3)`, ..., `f(n)` iteratively.
|
||
|
||
While this works and gives the right answer, it's O(n) time and O(1) or O(n) space — far more than needed.
|
||
|
||
Once you prove mathematically that `f(n) = 0.5` for all `n >= 2`, you can skip all computation.
|
||
wrong_approach: "Build DP table from 2 to n"
|
||
correct_approach: "Return 0.5 directly (after proving the pattern)"
|
||
|
||
- title: Forgetting the n = 1 Edge Case
|
||
description: |
|
||
When `n = 1`, the first passenger is also the last passenger. They pick their own seat (the only seat available), so probability is `1.0`, not `0.5`.
|
||
|
||
This is the only case that breaks the "always 0.5" pattern.
|
||
|
||
key_takeaways:
|
||
- "**Look for symmetry**: When two outcomes seem equally likely at every decision point, the final probabilities are often equal"
|
||
- "**Recursive problems can have closed-form solutions**: Don't stop at a working recurrence — ask if there's a pattern"
|
||
- "**Brainteasers reward insight over brute force**: This problem tests mathematical reasoning, not coding skill"
|
||
- "**The answer 0.5 is counterintuitive**: With 100 passengers, you'd expect the last person's chances to be tiny — but symmetry saves them"
|
||
|
||
time_complexity: "O(1). We return a constant value based on a simple condition."
|
||
space_complexity: "O(1). No additional data structures are used."
|
||
|
||
solutions:
|
||
- approach_name: Mathematical Insight
|
||
is_optimal: true
|
||
code: |
|
||
def nth_person_gets_nth_seat(n: int) -> float:
|
||
# Edge case: only one passenger, they get their own seat
|
||
if n == 1:
|
||
return 1.0
|
||
|
||
# For n >= 2, symmetry guarantees 50% probability
|
||
# Seats 1 and n are equally likely to be taken at any decision point
|
||
return 0.5
|
||
explanation: |
|
||
**Time Complexity:** O(1) — Single comparison and return.
|
||
|
||
**Space Complexity:** O(1) — No additional memory used.
|
||
|
||
This solution leverages the mathematical proof that for any `n >= 2`, the probability is exactly 0.5. The symmetry between seat 1 and seat n at every random choice guarantees this elegant result.
|
||
|
||
- approach_name: Dynamic Programming
|
||
is_optimal: false
|
||
code: |
|
||
def nth_person_gets_nth_seat(n: int) -> float:
|
||
# f(k) = probability last person gets seat with k passengers
|
||
# Base case: with 1 passenger, they get their seat
|
||
if n == 1:
|
||
return 1.0
|
||
|
||
# f(n) = 1/n + sum over k=2 to n-1 of (1/n * f(n-k+1))
|
||
# This simplifies to f(n) = 1/n + (1/n) * sum of f(2) to f(n-1)
|
||
|
||
# We can compute iteratively, but pattern emerges: f(k) = 0.5 for k >= 2
|
||
# Let's verify with actual DP
|
||
dp = [0.0] * (n + 1)
|
||
dp[1] = 1.0
|
||
|
||
for k in range(2, n + 1):
|
||
# Probability = 1/k (picks seat 1) + sum of subproblems
|
||
prob = 1.0 / k # First passenger picks their own seat
|
||
for j in range(2, k):
|
||
# First passenger picks seat j, creating subproblem of size k-j+1
|
||
prob += (1.0 / k) * dp[k - j + 1]
|
||
dp[k] = prob
|
||
|
||
return dp[n]
|
||
explanation: |
|
||
**Time Complexity:** O(n²) — Nested loops to compute each DP state.
|
||
|
||
**Space Complexity:** O(n) — DP array of size n+1.
|
||
|
||
This solution explicitly computes the recurrence relation. While correct, it's far slower than necessary. Running this reveals that `dp[k] = 0.5` for all `k >= 2`, validating the O(1) mathematical solution.
|
||
|
||
- approach_name: Recursive with Memoisation
|
||
is_optimal: false
|
||
code: |
|
||
from functools import lru_cache
|
||
|
||
def nth_person_gets_nth_seat(n: int) -> float:
|
||
@lru_cache(maxsize=None)
|
||
def probability(k: int) -> float:
|
||
# Base case: single passenger always gets their seat
|
||
if k == 1:
|
||
return 1.0
|
||
|
||
# First passenger picks seat 1: everyone gets their seat (prob 1/k)
|
||
# First passenger picks seat k: last person loses (prob 1/k, contributes 0)
|
||
# First passenger picks seat j (2 <= j < k): subproblem of size k-j+1
|
||
result = 1.0 / k # Picks seat 1
|
||
|
||
for j in range(2, k):
|
||
# Picks seat j, passenger j becomes "new first passenger"
|
||
result += (1.0 / k) * probability(k - j + 1)
|
||
|
||
return result
|
||
|
||
return probability(n)
|
||
explanation: |
|
||
**Time Complexity:** O(n²) — Each subproblem computed once, but summing takes O(n) per state.
|
||
|
||
**Space Complexity:** O(n) — Recursion stack and memoisation cache.
|
||
|
||
This recursive approach directly models the problem's structure. Memoisation prevents recomputation. Like the DP solution, it confirms the 0.5 pattern but is unnecessarily complex for the final answer.
|