DEV Community

Cover image for 52 Possibilities. 5 Cards. No Guessing. Here’s the Algorithm.
Sanu Khan
Sanu Khan

Posted on

52 Possibilities. 5 Cards. No Guessing. Here’s the Algorithm.

I recently came across a card trick that initially looked like ordinary magic.

Five cards are selected from a standard 52-card deck.

One card is hidden.

The other four are shown to another person in a carefully chosen order.

And somehow, from those four cards alone, they can determine exactly which fifth card is missing.

No marked cards.

No probability.

No machine learning.

No brute force.

Just mathematics.

What interested me wasn't really the trick itself.

It was how closely the solution resembles something we do constantly in software engineering:

Reduce the problem space before trying to compute the answer.

And the first step comes from one of the simplest ideas in discrete mathematics: the Pigeonhole Principle.


The Pigeonhole Principle

The classic definition is simple:

If you place more pigeons than pigeonholes, at least one pigeonhole must contain more than one pigeon.

If you put 5 pigeons into 4 holes, some hole must contain at least 2 pigeons.

Formally, distributing n objects among m containers where:

n > m
Enter fullscreen mode Exit fullscreen mode

guarantees that at least one container contains multiple objects.

More generally, at least one bucket contains:

ceil(n / m)
Enter fullscreen mode Exit fullscreen mode

objects.

That sounds almost too obvious to be useful.

But this tiny observation can give us surprisingly powerful guarantees.


Five Cards, Four Suits

Consider a standard deck.

There are four suits:

♠ Spades
♥ Hearts
♦ Diamonds
♣ Clubs
Enter fullscreen mode Exit fullscreen mode

Now select five cards.

We have:

Objects      = 5 cards
Buckets      = 4 suits
Enter fullscreen mode Exit fullscreen mode

Because:

5 > 4
Enter fullscreen mode Exit fullscreen mode

at least two cards must have the same suit.

Not probably.

Not usually.

Always.

For example:

3♠
9♠
K♥
5♦
7♣
Enter fullscreen mode Exit fullscreen mode

There are two spades.

That duplicated suit becomes our first piece of information.

Suppose we hide:

9♠
Enter fullscreen mode Exit fullscreen mode

and reveal:

3♠
Enter fullscreen mode Exit fullscreen mode

The person decoding the trick already knows something important:

Hidden card suit = ♠
Enter fullscreen mode Exit fullscreen mode

We have eliminated three quarters of the deck without explicitly communicating the suit.

But we still need the rank.

That's where the trick becomes much more interesting.


Turning 13 Ranks Into Only 6 Possibilities

A suit contains 13 ranks:

A 2 3 4 5 6 7 8 9 10 J Q K
Enter fullscreen mode Exit fullscreen mode

Instead of thinking about this as a line, think of it as a circular data structure:

A → 2 → 3 → ... → Q → K
↑                   ↓
└───────────────────┘
Enter fullscreen mode Exit fullscreen mode

Now take any two cards of the same suit.

There are two possible directions between them around this circle.

Those distances together equal 13.

Therefore, at least one direction must have a distance of at most 6.

So the person arranging the cards chooses which card to reveal and which to hide such that:

distance = 1..6
Enter fullscreen mode Exit fullscreen mode

Suddenly our problem has changed.

Originally we needed to identify one card among 52.

Now we already know the suit and only need to communicate one of:

1
2
3
4
5
6
Enter fullscreen mode Exit fullscreen mode

That is a dramatic reduction in the search space.


But How Do We Transmit 1–6?

Remember that five cards were originally selected.

Two are being used for our same-suit pair:

1 visible key card
1 hidden card
Enter fullscreen mode Exit fullscreen mode

That leaves three cards.

Three distinct objects can be arranged in:

3! = 3 × 2 × 1 = 6
Enter fullscreen mode Exit fullscreen mode

different ways.

Exactly six.

So the ordering of those three cards can represent the numbers 1 through 6.

Conceptually:

ABC → 1
ACB → 2
BAC → 3
BCA → 4
CAB → 5
CBA → 6
Enter fullscreen mode Exit fullscreen mode

The precise mapping doesn't matter as long as the encoder and decoder agree on it.

Suppose the first visible card is:

3♠
Enter fullscreen mode Exit fullscreen mode

and the remaining cards appear in the permutation representing:

+6
Enter fullscreen mode Exit fullscreen mode

The decoder performs:

3 + 6 = 9
Enter fullscreen mode Exit fullscreen mode

and therefore knows that the hidden card is:

9♠
Enter fullscreen mode Exit fullscreen mode

What looked like magic was actually a communication protocol.


This Is an Encoding Algorithm

Think about the roles involved.

The first person is an:

Encoder
Enter fullscreen mode Exit fullscreen mode

The second person is a:

Decoder
Enter fullscreen mode Exit fullscreen mode

The order of the cards is the:

Message
Enter fullscreen mode Exit fullscreen mode

And their shared understanding of the ordering convention is the:

Protocol
Enter fullscreen mode Exit fullscreen mode

That's remarkably close to what we build in software every day.

The cards aren't merely being displayed.

Their arrangement carries information.

In other words:

Physical state → encoded information
Enter fullscreen mode Exit fullscreen mode

That idea appears everywhere in computing.

A bit uses two states:

0
1
Enter fullscreen mode Exit fullscreen mode

Two bits provide:

00
01
10
11
Enter fullscreen mode Exit fullscreen mode

Four states.

Three bits provide eight states.

Likewise, three cards provide:

3! = 6
Enter fullscreen mode Exit fullscreen mode

possible ordering states.

The trick is essentially exploiting the information capacity of permutations.


Why This Matters for Software Engineers

When systems become computationally expensive, our first instinct is often:

"How do we calculate this faster?"

But a better question is frequently:

"How much of this do we actually need to calculate?"

That distinction matters enormously.

Consider a hypothetical search problem with:

1,000,000,000 candidates
Enter fullscreen mode Exit fullscreen mode

Throwing more CPU at the problem might make each comparison faster.

But suppose mathematical or domain constraints let us eliminate 99.999% of the candidates before searching.

Now we're dealing with perhaps:

10,000 candidates
Enter fullscreen mode Exit fullscreen mode

The biggest optimization wasn't faster hardware.

It was not performing unnecessary computation in the first place.

The card trick does exactly this.

It transforms:

Find 1 card among 52
Enter fullscreen mode Exit fullscreen mode

into approximately:

Determine suit from the key card
+
decode one value from 1..6
Enter fullscreen mode Exit fullscreen mode

The representation of the problem changes.

And once the representation changes, the computation becomes trivial.


Example 1: Hash Tables

Consider inserting users into buckets using a hash:

function bucketFor(userId, bucketCount) {
  return hash(userId) % bucketCount;
}
Enter fullscreen mode Exit fullscreen mode

Suppose:

users   = 1,000,000
buckets = 10,000
Enter fullscreen mode Exit fullscreen mode

The generalized pigeonhole principle tells us immediately that some buckets necessarily contain multiple users.

In fact, at least one bucket contains at least:

ceil(1,000,000 / 10,000)

= 100
Enter fullscreen mode Exit fullscreen mode

users.

This is why collisions aren't an unexpected failure of hashing.

They're mathematically inevitable whenever the input space exceeds the output space.

The engineering question therefore isn't:

Can collisions happen?
Enter fullscreen mode Exit fullscreen mode

It's:

How efficiently do we handle inevitable collisions?
Enter fullscreen mode Exit fullscreen mode

That leads directly to concepts such as chaining, open addressing, load factors and resizing.


Example 2: Database Partitioning and Sharding

Imagine distributing:

100 million customers
Enter fullscreen mode Exit fullscreen mode

across:

100 database shards
Enter fullscreen mode Exit fullscreen mode

Even under perfect distribution, we're looking at roughly:

1,000,000 customers / shard
Enter fullscreen mode Exit fullscreen mode

But real-world keys rarely distribute perfectly.

Understanding the number of objects relative to the number of buckets helps us reason about:

hot partitions
skew
capacity planning
partition-key design
rebalancing
Enter fullscreen mode Exit fullscreen mode

Consider:

const shard = customerId % SHARD_COUNT;
Enter fullscreen mode Exit fullscreen mode

This looks innocent.

But the quality of the partition key determines whether your load looks like:

Shard 1 → 1M
Shard 2 → 1M
Shard 3 → 1M
...
Enter fullscreen mode Exit fullscreen mode

or:

Shard 1 → 12M
Shard 2 → 300K
Shard 3 → 800K
...
Enter fullscreen mode Exit fullscreen mode

The mathematics gives us the lower-level guarantee.

Architecture determines whether the real-world distribution is useful.


Example 3: Scheduling and Resource Allocation

Suppose a system receives:

101 jobs
Enter fullscreen mode Exit fullscreen mode

and has:

10 workers
Enter fullscreen mode Exit fullscreen mode

At least one worker must receive:

ceil(101 / 10) = 11
Enter fullscreen mode Exit fullscreen mode

jobs.

Again, that's not a performance prediction.

It's a mathematical guarantee about distribution.

This kind of reasoning becomes useful when designing:

worker pools
queue consumers
thread pools
Kubernetes workloads
batch processors
rate-limited APIs
Enter fullscreen mode Exit fullscreen mode

Before benchmarking anything, mathematics can sometimes tell us what load conditions must exist.


Example 4: Duplicate Detection

Here's another interesting one.

Imagine generating identifiers containing only the numbers:

0000 - 9999
Enter fullscreen mode Exit fullscreen mode

There are exactly:

10,000
Enter fullscreen mode Exit fullscreen mode

possible IDs.

If your system creates:

10,001
Enter fullscreen mode Exit fullscreen mode

active records while requiring every ID to be unique, you don't need monitoring to determine whether a collision can occur.

A collision is guaranteed.

No amount of better random-number generation fixes this.

The namespace itself is insufficient.

This distinction is important:

Randomness ≠ uniqueness
Enter fullscreen mode Exit fullscreen mode

If the available state space is smaller than the number of objects that require unique states, collision avoidance becomes mathematically impossible.


Example 5: Distributed Systems

Suppose 50,000 requests must be assigned to 100 processing nodes.

The pigeonhole principle immediately tells us:

some node handles at least 500 requests
Enter fullscreen mode Exit fullscreen mode

That's merely the lower bound.

Real systems introduce skew through:

geography
tenant size
cache affinity
sticky sessions
partition keys
time-of-day traffic
retry storms
Enter fullscreen mode Exit fullscreen mode

So the practical maximum may be much larger.

But mathematical bounds are useful because they give architects something important before simulation even begins:

a guaranteed constraint.


High-Order Computation Isn't Always About More Compute

This is the larger lesson I took from the trick.

When we're dealing with large computational spaces—optimization problems, search algorithms, distributed workloads or AI systems—we often focus on increasing computational power.

More CPUs.

More GPUs.

More memory.

More workers.

More parallelism.

But algorithm design frequently wins by changing the problem before computation begins.

Suppose an algorithm searches n elements:

O(n)
Enter fullscreen mode Exit fullscreen mode

Reducing execution time by 50% is useful.

But suppose domain constraints reduce the candidate set from:

n
Enter fullscreen mode Exit fullscreen mode

to:

log(n)
Enter fullscreen mode Exit fullscreen mode

or allow us to restructure the algorithm entirely.

That's a fundamentally different optimization.

This is why computer science spends so much time on:

data structures
constraints
invariants
combinatorics
graph theory
probability
information theory
complexity analysis
Enter fullscreen mode Exit fullscreen mode

We aren't merely learning ways to make computers calculate.

We're learning ways to make computers calculate less.


Think About Search Algorithms

A linear search through one billion values potentially requires:

1,000,000,000 comparisons
Enter fullscreen mode Exit fullscreen mode

If the data is ordered, binary search reduces that to approximately:

log₂(1,000,000,000) ≈ 30
Enter fullscreen mode Exit fullscreen mode

That's astonishing.

We didn't build a CPU that is 33 million times faster.

We changed the structure of the problem.

Linear search:

1,000,000,000 possibilities
        ↓
 potentially 1,000,000,000 operations


Binary search:

1,000,000,000 possibilities
        ↓
 ~30 decisions
Enter fullscreen mode Exit fullscreen mode

The card trick follows the same philosophy.

Don't inspect every possible card.

Encode constraints into the representation until the remaining answer becomes trivial.


The Information-Theory Perspective

There's another layer to this.

When three cards are ordered, there are:

3! = 6
Enter fullscreen mode Exit fullscreen mode

possible messages.

The information capacity is therefore:

log₂(6) ≈ 2.585 bits
Enter fullscreen mode Exit fullscreen mode

That's enough information to distinguish among six possibilities.

So the magician and assistant have effectively constructed a tiny communication channel without saying anything.

This general principle appears throughout computing.

For n distinct objects, their ordering can represent:

n!
Enter fullscreen mode Exit fullscreen mode

states.

The information encoded by the permutation is approximately:

log₂(n!)
Enter fullscreen mode Exit fullscreen mode

bits.

As n grows, this becomes surprisingly large.

For example:

5!  = 120 states
10! = 3,628,800 states
20! ≈ 2.43 × 10¹⁸ states
Enter fullscreen mode Exit fullscreen mode

Permutation isn't merely ordering.

Ordering itself can be data.


This Pattern Appears Everywhere

Once you start looking for this idea, you see variations of it throughout software engineering.

Hashing
    ↓
Map a huge key space into manageable buckets.

Database indexes
    ↓
Avoid scanning the entire dataset.

Binary search
    ↓
Discard half the remaining state space per decision.

Bloom filters
    ↓
Use compact probabilistic state to avoid expensive lookups.

Caching
    ↓
Avoid recomputing known results.

Database partitioning
    ↓
Reduce the portion of data involved in an operation.

Branch-and-bound
    ↓
Eliminate entire regions of a search tree.

Dynamic programming
    ↓
Avoid solving identical subproblems repeatedly.

Constraint propagation
    ↓
Eliminate impossible states before exploring them.
Enter fullscreen mode Exit fullscreen mode

Different techniques.

Same broader philosophy:

Use information and constraints to eliminate unnecessary computation.


A Useful Engineering Question

When facing an expensive problem, we often ask:

How can we process this faster?
Enter fullscreen mode Exit fullscreen mode

I've started to think the more valuable sequence is:

What do we know?

↓

What can those constraints eliminate?

↓

How much information do we actually need?

↓

Can the representation encode some of that information?

↓

What remains to be computed?

↓

Only then: how do we compute it efficiently?
Enter fullscreen mode Exit fullscreen mode

That ordering matters.

Because sometimes the difference between an expensive problem and a trivial one isn't hardware.

It's discovering the right invariant.


From Five Cards to System Design

That's what I liked about this particular card trick.

Five cards.

Four suits.

One unavoidable duplicate.

Six possible distances.

Six permutations.

And suddenly a seemingly impossible prediction becomes deterministic.

Pigeonhole Principle
        ↓
Find guaranteed structure

Cyclic arithmetic
        ↓
Reduce rank distance to 1..6

Permutations
        ↓
Encode one of six possibilities

Decoder
        ↓
Reconstruct the hidden information
Enter fullscreen mode Exit fullscreen mode

There is no magic left once you understand the protocol.

But from an engineering perspective, I think what replaces the magic is considerably more interesting.


Final Thought

Some of the most powerful optimizations don't make computation faster.

They make computation unnecessary.

The pigeonhole principle is elementary mathematics, but combined with constraints, permutations and encoding, it can transform the structure of a problem.

That's equally true whether you're predicting a playing card or designing a distributed system.

Before scaling infrastructure, increasing parallelism or throwing more compute at a difficult problem, ask:

Is there a mathematical property of the problem that lets me reduce the state space first?

Sometimes the smartest computation is the one you successfully avoid.

Top comments (0)