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

    &nbsp;

    **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.

    &nbsp;

    **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)`

    &nbsp;

    **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`

    &nbsp;

    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.
