What Is the Pigeonhole Principle (And Why It Solves Hard Interview Problems)

June 6, 20269 min read
dsaalgorithmsinterview-prepleetcode
What Is the Pigeonhole Principle (And Why It Solves Hard Interview Problems)
TL;DR
  • The pigeonhole principle states that n items in m containers where n > m guarantees at least one container holds 2+ items; the generalized form gives ⌈n/m⌉
  • LeetCode 287 (Find the Duplicate): n+1 values in [1,n] guarantees a duplicate, turning the array into a functional graph with a cycle Floyd's detects
  • Finite alphabets cap sliding window size: any string longer than 26 characters must repeat a lowercase letter, bounding window width without inspection
  • LeetCode 523 (Subarray Sum Divisible by k): n+1 prefix sums vs k possible remainders guarantees a collision when n ≥ k, and that collision is the target subarray
  • The birthday paradox explains why hash tables rehash at 2/3 or 0.75 load factor, not when full — collision probability climbs faster than intuition expects
  • Spot pigeonhole problems by: "prove existence" language, finite value ranges, n+1 items in n buckets, divisibility + prefix sums, or O(1) space with bounded values

You've been there. Coding interview problem on the screen, timer running, brain cycling through every data structure you know. Then you see someone's solution. It's five lines. The "algorithm" is one sentence of reasoning. The trick wasn't a clever tree traversal or a segment tree lurking in the background. It was counting.

That's the pigeonhole principle. It lets you prove a duplicate exists before searching for it, bound a sliding window without inspecting it, and guarantee a subarray exists before computing it. And once you internalize it, you'll start spotting it everywhere, including on that problem that just humiliated you in a loop.

What the Pigeonhole Principle Actually Says

The formal statement: if you have n items and m containers, and n > m, then at least one container must hold at least 2 items.

That's it. That's the whole thing. Johann Peter Gustav Lejeune Dirichlet formalized this in 1834 under the name "Schubfachprinzip" (German for "drawer principle"), and mathematicians have been using it to feel very smart ever since. The pigeonhole name came later, presumably because nobody wanted to say "Schubfachprinzip" in a lecture.

The generalized version is more useful in practice. If you distribute n items across m containers, at least one container holds at least ⌈n/m⌉ items. This lets you make precise claims, not just "at least 2."

The reason this matters in interviews: it lets you prove something exists without finding it. You count first, conclude a collision is inevitable, then write code to locate the thing you've already proven has to be there.

Find the Duplicate Number: Proving Before Searching

LeetCode 287 gives you an array of n+1 integers where every value is in the range [1, n]. Find the duplicate. Constraints: O(1) extra space, so no hash set.

If you've seen this problem and felt like the solution came out of nowhere, that's because you were missing one sentence. Here it is:

You have n+1 values stuffed into n possible buckets (the integers 1 through n). More items than buckets. A duplicate is not a possibility. It is a mathematical certainty.

The interviewer already knew this. They were waiting to see if you did.

That counting argument is the hard part. The rest is implementation. Because every value in [1, n] is a valid index into the array, you can read the whole thing as a linked list where index i points to nums[i]. The duplicate value is the only node two things point at, which makes it the entry point of a cycle. That's exactly what Floyd's cycle detection was built for.

def findDuplicate(nums: list[int]) -> int: slow = nums[0] fast = nums[0] while True: slow = nums[slow] fast = nums[nums[fast]] if slow == fast: break slow = nums[0] while slow != fast: slow = nums[slow] fast = nums[fast] return slow

Without the pigeonhole argument, Floyd's here looks like someone pulled a rabbit out of a hat. With it, you know why the functional graph must have a cycle. You're not doing magic. You're doing math, then code.

The 26-Letter Trick: Strings and Finite Alphabets

The English lowercase alphabet has exactly 26 letters. A string with more than 26 characters must repeat one. There are only 26 buckets. The 27th character has nowhere new to go.

Stated that bluntly, it sounds obvious. And yet.

def has_repeated_char(s: str) -> bool: return len(s) > 26

That's the complete function for lowercase ASCII. The entire body is a comparison. No loop. No set. No nothing.

In a sliding window problem, this becomes useful fast: any window of length greater than 26 cannot contain all unique characters. You can pair this with the sliding window algorithm to put a hard cap on your window size without ever needing to inspect the contents.

The bound on alphabet size converts an open-ended search into a guaranteed result. When a problem says "all unique characters" and the alphabet is finite, you immediately know the maximum possible window length. Write that down before you write a single line of code.

Subarray Sums and Modular Arithmetic

LeetCode 523 asks whether any continuous subarray sums to a multiple of k. This one buries the pigeonhole argument a few layers deep behind some algebra, which is probably why it catches people off guard.

Compute prefix sums mod k. You get prefix[0] through prefix[n]: that's n+1 values. Each value is a remainder in [0, k-1], which gives you exactly k possible buckets.

When n+1 > k (when your array is at least as long as k), you have more prefix sums than possible remainders. Two prefix sums must share the same remainder. Any two prefix sums sharing a remainder define a subarray whose sum is divisible by k.

The math: prefix[j] - prefix[i] is your subarray sum. If prefix[j] % k == prefix[i] % k, then (prefix[j] - prefix[i]) % k == 0. Guaranteed. Before you've checked a single subarray.

def checkSubarraySum(nums: list[int], k: int) -> bool: remainder_to_index = {0: -1} prefix = 0 for index, num in enumerate(nums): prefix = (prefix + num) % k if prefix in remainder_to_index: if index - remainder_to_index[prefix] >= 2: return True else: remainder_to_index[prefix] = index return False

The pigeonhole argument tells you the collision must exist when the array is long enough. The hash map finds it efficiently. Keep those two jobs separate in your head. The explanation is much cleaner when you can say "I know a collision exists because..." before explaining where to look.

The Birthday Paradox and Hash Collisions

The birthday paradox is the pigeonhole principle wearing a probability hat.

You need 366 people in a room to be certain two share a birthday. One person per day, 365 days, the 366th has nowhere to go. Fine. But you only need 23 people to have a better-than-even chance. That's the part that breaks people's intuition. Certainty takes 366. Likely takes 23.

This is why hash tables behave the way they do. A hash function maps keys to buckets, and collisions start happening long before the table fills up. The birthday paradox math explains precisely how fast that probability climbs as the load factor rises.

That's why Python dicts rehash at 2/3 capacity and Java HashMaps rehash at 0.75. Not when full. Not halfway. At those specific thresholds, chosen because the birthday math says collision probability starts compounding badly past that point. Read more about why collisions are unavoidable and how tables manage them in hash collision handling.

The interviewer who asks "why does a hash map rehash at 75%?" is asking a birthday paradox question. Most candidates say "to keep performance good." The correct answer has a number derivation behind it.

How to Spot Pigeonhole Principle Problems in a Coding Interview

A few signals that counting is the intended insight:

  1. "Prove a duplicate exists" or "at least one" language. If the problem guarantees a collision must exist, it's almost certainly a counting argument.
  2. Finite alphabet or finite range. When values are constrained to [1, n] or lowercase letters, count the buckets and compare to the items.
  3. n+1 items in a range of size n. This is the direct hit. See it, name it immediately, out loud.
  4. Divisibility questions with prefix sums. The n+1 prefix sums vs k remainders pattern is reusable exactly as stated above.
  5. "In O(1) extra space" combined with bounded values. When you can't use a set, the problem is often nudging you toward counting instead.

When you spot any of these, stop writing code. Start counting. Say it out loud: "I have X items and Y possible values, so..." The interviewer will nod. That nod means you understand the problem structurally, not just procedurally. It's a different category of signal than someone who got the code right by intuition and can't explain it.

Why This Matters Beyond These Specific Problems

The pigeonhole principle is one of a small handful of mathematical tools that surface repeatedly across algorithm problems. Induction. Contradiction. Amortized analysis. Pigeonhole. Once you internalize these, you stop treating interview problems as isolated puzzles and start recognizing argument types. That's a real skill upgrade, not a flashcard trick.

The bigger skill is being able to say "a solution must exist because..." before explaining how to find it. Two-phase reasoning: existence proof, then algorithm. That structure separates a clear explainer from someone who reverse-engineers the solution from the code and hopes the interviewer follows along.

In a live interview, it pays off concretely. Instead of "I'm going to try Floyd's cycle detection here," you say "by the pigeonhole principle, a duplicate is guaranteed, which means this array encodes a functional graph with a cycle, so I'll use Floyd's to find the cycle entry." That's a complete argument. Easy to write up in feedback. Signals mathematical maturity without being the person who says "Schubfachprinzip" unprompted.

Practicing spoken reasoning under pressure, not just writing code, is what SpaceComplexity is built for. You can get the logic right in your head and still fumble explaining it. The interview is exactly where that gap shows up.

Key Takeaways

  • The pigeonhole principle: n items into m containers where n > m means at least one container holds at least 2 items. Generalized: at least ⌈n/m⌉.
  • LeetCode 287: n+1 values in [1, n] guarantees a duplicate. Every value is a valid index, so the array encodes a functional graph with a cycle. Floyd's finds the entry.
  • Any string longer than 26 characters must repeat a lowercase letter. Use this to cap sliding window size without inspecting the window.
  • LeetCode 523: n+1 prefix sums mod k, only k possible remainders. When n is at least k, a collision is guaranteed, and that collision is your target subarray.
  • The birthday paradox explains why hash tables rehash at 2/3 or 0.75 load, not when full. Collision probability climbs faster than intuition suggests.
  • Spot it by: "prove existence," finite value range, n+1 items in n buckets, divisibility + prefix sums, O(1) space + bounded values. State the count before writing code.

Further Reading