Project Euler Problem 253

A small child has a “number caterpillar” consisting of forty jigsaw pieces, each with one number on it, which, when conn

Project Euler Problem 253

Solution

Answer: 11.492847

Let the caterpillar have $n$ pieces arranged on positions $1,2,\dots,n$.

We place the pieces in a uniformly random order.

At every stage, the occupied positions form several contiguous blocks (“segments”).

We are interested in

$$M = \max(\text{number of segments seen during the process}).$$

For $n=40$, we must compute

$$\mathbb E[M].$$


1. Mathematical analysis

Segment dynamics

Suppose we place a piece at position $x$.

There are only three possibilities:

  • neither neighbor is occupied

$\Rightarrow$ a new segment is created

$\Rightarrow \Delta S = +1$

  • exactly one neighbor is occupied

$\Rightarrow$ an existing segment is extended

$\Rightarrow \Delta S = 0$

  • both neighbors are occupied

$\Rightarrow$ two segments merge

$\Rightarrow \Delta S = -1$

Thus the process is completely determined by the pattern of empty gaps.


Gap representation

Instead of storing occupied positions, we store empty gaps.

A gap is represented by

$$(g,L,R)$$

where:

  • $g$ = number of empty cells in the gap
  • $L \in {0,1}$: is the left boundary occupied?
  • $R \in {0,1}$: is the right boundary occupied?

Initially:

$$[(40,0,0)]$$

(one completely empty interval).

If we place a piece inside a gap, the gap splits into two smaller gaps.

This gives a clean recursive DP.


Probability DP

Define

$$F(state,s,m)$$

as the probability that, starting from:

  • current gap configuration = state
  • current number of segments = $s$

we never exceed $m$ segments in the future.

Then:

  • if $s>m$, return $0$
  • if no gaps remain, return $1$

Otherwise average over all possible placements.

Finally,

$$P(M \le m)=F(\text{initial},0,m).$$

Using the identity

$$\mathbb E[M] = \sum_{k\ge1} P(M\ge k) = \sum_{k\ge1} \left(1-P(M\le k-1)\right),$$

we obtain the expectation exactly.


2. Python implementation

from functools import cache
from collections import defaultdict

N = 40

@cache
def transitions(state, segs):
    """
    Generate all transitions from a given state.

    state = tuple of gaps (g, L, R)
    segs  = current number of segments
    """

    total_empty = sum(g for g, _, _ in state)

    out = defaultdict(int)

    for idx, (g, L, R) in enumerate(state):

        others = list(state[:idx] + state[idx + 1:])

        for pos in range(1, g + 1):

            # Number of occupied neighbours
            c = 0
            if pos == 1 and L:
                c += 1
            if pos == g and R:
                c += 1

            # Segment count change
            if c == 0:
                new_segs = segs + 1
            elif c == 1:
                new_segs = segs
            else:
                new_segs = segs - 1

            new_state = others[:]

            left_len = pos - 1
            right_len = g - pos

            if left_len > 0:
                new_state.append((left_len, L, 1))

            if right_len > 0:
                new_state.append((right_len, 1, R))

            new_state = tuple(sorted(new_state))

            out[(new_state, new_segs)] += 1

    return total_empty, list(out.items())

def probability_max_at_most(m):

    @cache
    def F(state, segs):

        if segs > m:
            return 0.0

        if not state:
            return 1.0

        total_empty, trans = transitions(state, segs)

        ans = 0.0

        for (new_state, new_segs), count in trans:
            ans += count * F(new_state, new_segs)

        return ans / total_empty

    return F(((N, 0, 0),), 0)

# Compute expectation
expectation = 0.0

for k in range(1, N + 1):
    p_le = probability_max_at_most(k - 1)
    expectation += 1.0 - p_le

print(f"{expectation:.6f}")

3. Code walkthrough

transitions(state, segs)

For every empty position in every gap:

  • determine how many occupied neighbors it touches
  • update segment count
  • split the gap into left/right subgaps
  • aggregate identical resulting states

This drastically reduces the state space.


F(state, segs)

Recursive memoized probability:

$$F = \Pr(\text{never exceed } m)$$

Base cases:

  • already exceeded $m$

$\Rightarrow 0$

  • no empty cells left

$\Rightarrow 1$

Otherwise average over all possible next placements.


Final expectation

We use

$$\mathbb E[M] = \sum_{k=1}^{N} \left(1-P(M\le k-1)\right).$$


4. Verification on the $n=10$ example

The program reproduces:

$M$ Count
1 512
2 250912
3 1815264
4 1418112
5 144000

and therefore

$$\mathbb E[M] = 3.400732$$

matching the statement.

So the DP is correct.


5. Final result

For $n=40$,

$$\mathbb E[M] = 11.492847\ldots$$

rounded to six decimal places.

Answer: 11.492847