Chapter 143
Two Cards at Lunch and the Multiplication Game
The Riddler for April 7, 2017. The Express is a two-step logic puzzle in the spirit of Sum and Product: each student’s “I don’t know” announcement is enough information to identify the loser’s card; the answer is . The Classic is a stochastic placement game where you fill a multiplication grid with cards drawn one at a time from a stack of to ; the optimal expected product is about , against for random play.
Riddler Express
A teacher hands each of two students a card with a positive integer on it. The product of the two numbers is , or . The students announce in turn whether they can deduce the other’s number; the first to deduce wins. The first student looks at her card and says, “I don’t know what your number is.” The second student looks at her card and says, “I don’t know what your number is either.” The first then says, “Now I know your number.” What number is on the loser’s card?
The Riddler, FiveThirtyEight, April 7, 2017(original post)
Solution
The possible numbers, listed by their divisor sets within , are For each number in this set, the set of compatible partners is determined by which of the product can be. The reasoning proceeds in three rounds, each one shrinking the candidate set.
Round 1: student A says “I don’t know.” If her card had only one possible partner, she would know it immediately. So her card has at least two compatible partners. The number of partners by candidate is:
| compatible partners (with ) | |
|---|---|
The rows with a unique partner are . So A’s card is one of .
Round 2: student B says “I don’t know either.” Knowing now that , B updates his partner set: is compatible iff some has . He must hold at least two such candidates, or he would know. Updated partner counts:
| possible given Round 1 | |
|---|---|
| (intersect ): | |
The unique-partner rows here are . The only whose partner set has at least two elements is (partners ). So B holds .
Round 3: A now knows. A’s card must have as one of its compatible partners. Looking at the original table, is a partner only for . So A holds either or , and in either case her partner is . A deduces correctly that B’s card is . A wins; B is the loser.
The loser’s card is
The computation
Encode the problem itself: enumerate all pairs with product in , run the three rounds of common-knowledge updating exactly as a logician would, and report the value B must hold.
Build the set of candidate pairs.
Round 1: keep only pairs where has more than one compatible in the current set.
Round 2: keep only pairs where has more than one compatible in the surviving set.
Round 3: A’s card identifies B’s uniquely; report B’s number.
def partners(pairs, side):
out = {}
for a, b in pairs:
k, v = (a, b) if side == "A" else (b, a)
out.setdefault(k, set()).add(v)
return out
pairs = {(a, b) for a in range(1, 19) for b in range(1, 19)
if a * b in (12, 15, 18)}
# Round 1: A's "I don't know"
A_part = partners(pairs, "A")
pairs = {(a, b) for a, b in pairs if len(A_part[a]) > 1}
# Round 2: B's "I don't know"
B_part = partners(pairs, "B")
pairs = {(a, b) for a, b in pairs if len(B_part[b]) > 1}
# Round 3: A now knows. B's card is the unique surviving b.
B_card = {b for _, b in pairs}
print(f"B's card (the loser's) = {B_card}")
# B's card (the loser's) = {6}
Riddler Classic
You have a stack of ten cards printed with the digits through , one per card. Shuffled, you draw them one at a time and place each, sight unseen of the rest, into one of four slots in the equation where means . Once placed, a digit cannot be moved; only four of the ten digits end up in the equation. Your goal is to minimise the expected product. What is the optimal strategy and how much better is it than random placement?
The Riddler, FiveThirtyEight, April 7, 2017(original post)
Solution
The product is The dominant term is , so the strategy must keep small. The next-largest terms are and , and the cross terms involve and once each, so a small digit also wants to go into the tens place on the other row. The ones-place digits and pay weight in and weight in mixed terms, so they should be the largest remaining digits.
The structure of an optimal policy is straightforward in outline:
small digits prefer the tens places ,
large digits prefer the ones places .
But the policy depends on the joint distribution of the remaining cards, and the threshold for “small” shifts as cards are placed. The clean expression of the optimum is a recursion over the state , computed by backward induction. We give the recursion and report its value.
Let be the minimum expected product starting from state , where is the set of cards still in the stack and is the tuple of currently-placed digits in slots with marking an empty slot. The terminal value is . The Bellman recursion is Computing over all states with memoisation gives the exact optimum. The state space is small ( subsets times slot configurations, with most states unreachable), so the recursion is tractable.
The numerical values are where the exact rational sits over a denominator depending on the order of decisions, and the random-placement value (any permutation of of the digits into the slots, uniformly) is using by symmetry of the four slots, and for two distinct uniform draws from .
The boxed answer:
A clean heuristic that captures the first move exactly: place the first card in a tens slot ( or ) if it is or less, and in a ones slot ( or ) if it is or more. Subsequent moves depend on the partially-filled equation and the remaining stack; the full table is what the recursion computes.
The computation
Encode the recursion itself. Build by memoised search over all states, evaluating the four placement choices at every step.
from functools import lru_cache
ALL = frozenset(range(10))
@lru_cache(maxsize=None)
def V_state(remaining, slots):
"""Expected product with optimal play from this state."""
if all(s is not None for s in slots):
A, B, C, D = slots
return (10*A + B) * (10*C + D)
n = len(remaining)
total = 0
for c in remaining:
rem2 = remaining - {c}
best = None
for i in range(4):
if slots[i] is None:
new_slots = list(slots); new_slots[i] = c
v = V_state(rem2, tuple(new_slots))
if best is None or v < best:
best = v
total += best
return total / n
# Optimal expected product from the start
opt = V_state(ALL, (None, None, None, None))
print(f"optimal expected product = {opt:.4f}")
# Random: average product over all permutations of 4 of the 10 digits
from itertools import permutations
rand_total = 0; rand_count = 0
for A, B, C, D in permutations(range(10), 4):
rand_total += (10*A + B) * (10*C + D)
rand_count += 1
rand = rand_total / rand_count
print(f"random expected product = {rand:.4f}")
print(f"reduction = {(rand - opt) / rand * 100:.2f}%")
# optimal expected product = 1056.8379
# random expected product = 2339.3333
# reduction = 54.82%