Library · The Fiddler: Solutions · Chapter 133

Can You Make the Connection?

Revised Report an error

In the word game Connections, 1616 words must be sorted into four sets of four, each set sharing some common property. On each turn you select four of the remaining words and submit them. If they form one of the sets, they are removed and you look for the next. The game also gives a hint: whenever your four words include exactly three from one set, it says “One away…”.

Faced with all 1616 words, suppose you pick four at random. What is the probability that you see the “One away…” message?

The Fiddler, Zach Wissner-Gross, December 8, 2023(original post)

The official solution appeared in the post of December 15, 2023 and gives 48/455≈10.5548/455 \approx 10.55 percent for the main puzzle and 77 guesses for the Extra Credit. Both agree with the answers below.

Solution

Every set of four words is equally likely to be picked, so the answer is a count of good picks divided by a count of all picks.

All picks: choosing 44 of 1616 words can be done in (164)=16⋅15⋅14⋅134⋅3⋅2⋅1=1820\binom{16}{4} = \frac{16 \cdot 15 \cdot 14 \cdot 13}{4 \cdot 3 \cdot 2 \cdot 1} = 1820 ways.

Good picks have exactly three words from one set. Build one in three steps: choose the set that supplies the three words (44 ways), choose which of its four words to leave out (44 ways), and choose the fourth word from the 1212 words of the other three sets (1212 ways). Each good pick is built exactly once, because the set supplying three words is determined by the pick. So there are 4⋅4⋅12=1924 \cdot 4 \cdot 12 = 192 good picks, and  P(“One away”)  =  1921820  =  48455  ≈  10.55%. \boxed{\ P(\text{``One away''}) \;=\; \frac{192}{1820} \;=\; \frac{48}{455} \;\approx\; 10.55\%. \ } That is more than one time in ten, which is more often than most people guess.

The computation

The program labels the 1616 words by set, runs through all 18201820 picks of four, and counts those in which some set supplies exactly three words.

from fractions import Fraction
from itertools import combinations
from collections import Counter

words = [s for s in range(4) for _ in range(4)]  # word -> set
picks = list(combinations(range(16), 4))
hits = sum(1 for p in picks
           if 3 in Counter(words[i] for i in p).values())
print(len(picks), hits, Fraction(hits, len(picks)))

It prints

1820 192 48/455

Extra Credit

You have found two sets in your first two guesses, leaving eight words, and you have no idea how they split into the last two sets. Ignoring the game’s limit on wrong guesses, and using the “One away…” hints cleverly, how many guesses do you need to be certain of solving the puzzle in the worst case?

Solution

First, size up the unknown. Call the eight words AA to HH. The split is settled once we know which three words share a set with AA, so there are (73)=35\binom73 = 35 possible splits.

A guess of four words has three possible replies. If the four words are one of the sets, the guess is correct. Otherwise the four words meet the two sets either three and one (“One away”) or two and two (no message). Note that a guess tells us the same about its complement, the other four words.

At least seven guesses. Picture any strategy as a tree. Each guess is a node, and the two replies “One away” and “two-two” lead to two further nodes. A “correct” reply ends the search, and it can happen at a given node for only one of the 3535 splits, the split whose set was guessed. Every split must eventually be guessed correctly, so every split needs a node of its own. A tree with the first guess at the top and at most dd levels has at most 1+2+4+⋯+2d−1=2d−11 + 2 + 4 + \dots + 2^{d-1} = 2^d - 1 nodes. Five levels give only 3131 nodes, too few for 3535 splits, so some split is not found until the sixth guess at the earliest. One more guess submits the last four words. So no strategy can promise fewer than 77 guesses.

Seven guesses suffice. The counting argument leaves little slack (3535 splits against 6363 nodes in six levels), and it does not by itself produce a strategy. Each guess must split the surviving possibilities almost evenly. The first guess already shows the shape: guessing ABCDABCD leaves 1818 splits behind the two-two reply and 1616 behind “One away”, and both fit in the 3131 nodes of the five remaining levels. Pushing this through by hand level by level is laborious. The exhaustive search below tries every guess at every stage and confirms that a strategy exists that always finds the first set by the sixth guess. Together with the lower bound,  7 guesses in the worst case. \boxed{\ 7 \text{ guesses in the worst case}. \ }

The computation

The program solves the guessing game exactly. A state is the list of splits still consistent with the replies so far. For a state it tries every possible guess, sorts the surviving splits by the reply each would produce, and takes the worst branch. The best guess is the one whose worst branch is smallest. Once a single split is left, two more guesses finish the game. Caching the states keeps the search to about a second.

from functools import lru_cache

letters = range(8)
# word 0's set fixes the split: 35 possible splits
splits = [frozenset(c) for c in combinations(letters, 4)
          if 0 in c]
guesses = [frozenset(c) for c in combinations(letters, 4)]

def reply(g, s):
    k = len(g & s)
    if k in (0, 4):
        return "correct"          # g is one of the two sets
    return "one away" if k in (1, 3) else "two-two"

@lru_cache(maxsize=None)
def need(S):
    """Worst-case guesses still needed when the split
    is one of those in S."""
    if len(S) == 1:
        return 2                  # submit both sets
    best = float("inf")
    for g in guesses:
        groups = {}
        for s in S:
            groups.setdefault(reply(g, s), []).append(s)
        if len(groups) == 1 and "correct" not in groups:
            continue              # learns nothing
        worst = max(1 if r == "correct"
                    else need(frozenset(v))
                    for r, v in groups.items())
        best = min(best, 1 + worst)
    return best

print("worst-case guesses:", need(frozenset(splits)))

It prints

worst-case guesses: 7
Report an error on this page

Reports are stored by Netlify. See the privacy note.