Project Euler Problem 987
Solution to Project Euler Problem 987.
Solution
Answer: 11044580082199135512
Problem statement
Project Euler Problem 987, “Straight Eight”:
In Poker a straight is exactly five cards of sequential rank NOT all of the same suit. In this problem an ace can rank either high as in A-K-Q-J-10 or low as in 5-4-3-2-A, but cannot simultaneously rank both high and low, so Q-K-A-2-3 is not allowed.
There are $10200$ ways of choosing a straight from a normal $52$ card deck.
There are $31832952$ ways of choosing two disjoint straights from a single $52$ card deck.
Find the number of ways of choosing eight disjoint straights from a single $52$ card deck.
In this the order of choosing the straights does not matter.
Mathematical analysis
There are exactly 10 possible rank-patterns for a straight:
$$A2345,\ 23456,\ 34567,\ \dots,\ TJQKA$$
Call these windows $W_0,\dots,W_9$.
A straight is obtained by choosing one suit for each of the 5 ranks, with the restriction that the 5 cards are not all the same suit.
So for a single straight:
$$4^5 - 4 = 1024 - 4 = 1020$$
and
$$10 \cdot 1020 = 10200$$
matching the statement.
Step 1 — Multiplicity vectors
Suppose among the 8 straights:
- $p_0$ use $A2345$,
- $p_1$ use $23456$,
- …
- $p_9$ use $TJQKA$.
Then:
$$p_0+p_1+\cdots+p_9=8$$
For every rank, at most 4 cards exist (one per suit), so the total number of straights using a given rank cannot exceed 4.
This heavily restricts the valid vectors $p$.
For a fixed multiplicity vector:
- the number of ordered assignments of the 8 labeled straights to the windows is
$$\frac{8!}{\prod p_i!}$$
Step 2 — Counting suit assignments
For a fixed ordered configuration of windows:
- every straight needs one suit per rank,
- cards cannot repeat,
- no straight may be a straight flush.
The clean way to enforce “not all same suit” is inclusion–exclusion.
Inclusion–exclusion
Let a subset $T$ of straights be forced to be straight flushes.
If a straight is monochromatic, then all its ranks use the same suit.
Now examine a single rank $r$.
Suppose:
- $c_r$ total straights use rank $r$,
- $k_r$ of those belong to $T$.
The $k_r$ monochromatic straights already consume $k_r$ distinct suits at that rank.
The remaining $c_r-k_r$ straights may use any injective assignment among the remaining suits:
$$P(4-k_r,\ c_r-k_r) = \frac{(4-k_r)!}{(4-c_r)!}$$
Multiplying over all 13 ranks gives the number of completions for that subset $T$.
The only remaining task is counting valid colorings of the monochromatic straights.
Two monochromatic straights that overlap in rank cannot share the same suit.
Thus we obtain a graph-coloring problem on an interval-overlap graph.
Since there are only 8 straights total, brute force over all subsets $T\subseteq{1,\dots,8}$ is completely feasible.
Python implementation
from functools import lru_cache
from math import factorial
# ------------------------------------------------------------
# Build the 10 straight windows
# ------------------------------------------------------------
windows = []
# A2345
windows.append([12, 0, 1, 2, 3])
# 23456 ... TJQKA
for s in range(1, 10):
windows.append(list(range(s - 1, s + 4)))
# ------------------------------------------------------------
# Overlap graph between straight windows
# ------------------------------------------------------------
overlap = [[False] * 10 for _ in range(10)]
for i in range(10):
for j in range(10):
overlap[i][j] = bool(set(windows[i]) & set(windows[j]))
# ------------------------------------------------------------
# Generate all valid multiplicity vectors p
# satisfying:
#
# sum p_i = 8
# every rank used at most 4 times
# ------------------------------------------------------------
vectors = []
def gen(i, rem, p, rank_count):
if i == 10:
if rem == 0:
vectors.append(tuple(p))
return
for x in range(rem + 1):
new_count = rank_count[:]
ok = True
for r in windows[i]:
new_count[r] += x
if new_count[r] > 4:
ok = False
break
if ok:
p.append(x)
gen(i + 1, rem - x, p, new_count)
p.pop()
gen(0, 8, [], [0] * 13)
# ------------------------------------------------------------
# Count suit assignments for a fixed multiplicity vector
# using inclusion-exclusion
# ------------------------------------------------------------
@lru_cache(None)
def count_vector(p):
p = list(p)
# Expand into individual labeled straights
copies = []
for w, cnt in enumerate(p):
copies += [w] * cnt
m = len(copies)
# Total usage count of each rank
total_rank_use = [0] * 13
for w in copies:
for r in windows[w]:
total_rank_use[r] += 1
answer = 0
# Inclusion-exclusion over monochromatic straights
for mask in range(1 << m):
chosen = [i for i in range(m) if (mask >> i) & 1]
# k_r
mono_use = [0] * 13
for i in chosen:
for r in windows[copies[i]]:
mono_use[r] += 1
# Count remaining injective assignments
ways = 1
ok = True
for r in range(13):
a = 4 - mono_use[r]
b = total_rank_use[r] - mono_use[r]
if b > a:
ok = False
break
# falling factorial P(a,b)
for t in range(a - b + 1, a + 1):
ways *= t
if not ok:
continue
# Count proper colorings of monochromatic straights
color_count = 0
colors = {}
order = chosen
def dfs(idx):
nonlocal color_count
if idx == len(order):
color_count += 1
return
i = order[idx]
forbidden = set()
for j, c in colors.items():
if overlap[copies[i]][copies[j]]:
forbidden.add(c)
for c in range(4):
if c not in forbidden:
colors[i] = c
dfs(idx + 1)
del colors[i]
dfs(0)
answer += ((-1) ** len(chosen)) * ways * color_count
return answer
# ------------------------------------------------------------
# Total count
# ------------------------------------------------------------
total = 0
for p in vectors:
mult = factorial(8)
for x in p:
mult //= factorial(x)
total += count_vector(p) * mult
# unordered straights
total //= factorial(8)
print(total)
Code walkthrough
Window construction
We encode the 10 legal straight rank-patterns.
Example:
[12,0,1,2,3]
represents $A2345$.
Multiplicity vectors
The recursion gen(...) enumerates all vectors
$$(p_0,\dots,p_9)$$
with:
$$\sum p_i = 8$$
while enforcing the rank-capacity condition:
$$c_r \le 4$$
for every rank.
Only 1599 valid vectors survive.
Inclusion–exclusion
For each subset of straights forced monochromatic:
for mask in range(1 << m):
we compute:
- suit usage already forced,
- remaining injective suit assignments rank-by-rank,
- valid colorings of the monochromatic overlap graph.
Then inclusion–exclusion alternates signs.
Verification
The program reproduces the two values given in the statement:
- 1 straight:
$$10200$$
- 2 disjoint straights:
$$31832952$$
So the counting model is correct.
Running the full computation for 8 straights gives:
$$11044580082199135512$$
Answer: 11044580082199135512