Project Euler Problem 367

Bozo sort, not to be confused with the slightly less efficient bogo sort, consists out of checking if the input sequence

Project Euler Problem 367

Solution

Answer: 48271207

We model the process as a Markov chain on permutation cycle types.

A shuffle step works as follows:

  • Choose 3 positions uniformly from the $\binom{n}{3}$ triples.
  • Randomly permute the values in those positions using one of the $3!=6$ permutations.
  • Repeat until sorted.

Because the transition probabilities depend only on the permutation’s cycle structure, we can compress the state space from $11!$ permutations down to the 56 integer partitions of 11.

Here is a complete Python program that computes the exact expectation.

from itertools import combinations, permutations
from collections import Counter
from fractions import Fraction
import math

# ------------------------------------------------------------
# Cycle type of a permutation
# ------------------------------------------------------------
def cycle_type(p):
    n = len(p)
    seen = [False] * n
    parts = []

    for i in range(n):
        if not seen[i]:
            j = i
            length = 0

            while not seen[j]:
                seen[j] = True
                j = p[j]
                length += 1

            parts.append(length)

    return tuple(sorted(parts, reverse=True))

# ------------------------------------------------------------
# Generate all integer partitions of n
# ------------------------------------------------------------
def partitions(n, mx=None):
    if mx is None or mx > n:
        mx = n

    if n == 0:
        yield ()
    else:
        for k in range(mx, 0, -1):
            for rest in partitions(n - k, k):
                yield (k,) + rest

# ------------------------------------------------------------
# Representative permutation for a cycle type
# ------------------------------------------------------------
def representative(part):
    n = sum(part)
    p = list(range(n))

    start = 0
    for length in part:
        cyc = list(range(start, start + length))

        for i in range(length):
            p[cyc[i]] = cyc[(i + 1) % length]

        start += length

    return tuple(p)

# ------------------------------------------------------------
# Apply a 3-position shuffle
# ------------------------------------------------------------
def apply_shuffle(p, inds, sigma):
    q = list(p)
    old = [p[i] for i in inds]

    for t, i in enumerate(inds):
        q[i] = old[sigma[t]]

    return tuple(q)

# ------------------------------------------------------------
# Size of a conjugacy class
# ------------------------------------------------------------
def class_size(part):
    n = sum(part)

    cnt = Counter(part)

    denom = 1
    for k, v in cnt.items():
        denom *= (k ** v) * math.factorial(v)

    return math.factorial(n) // denom

# ------------------------------------------------------------
# Solve expectation system
# ------------------------------------------------------------
def expected_value(n):

    parts = list(partitions(n))
    index = {p: i for i, p in enumerate(parts)}

    reps = {p: representative(p) for p in parts}

    m = len(parts)

    triples = list(combinations(range(n), 3))
    sigmas = list(permutations(range(3)))

    total_moves = len(triples) * len(sigmas)

    # Transition matrix
    P = [[Fraction(0, 1) for _ in range(m)] for _ in range(m)]

    for part in parts:

        p = reps[part]
        counts = Counter()

        for inds in triples:
            for sigma in sigmas:
                q = apply_shuffle(p, inds, sigma)
                counts[cycle_type(q)] += 1

        i = index[part]

        for target, c in counts.items():
            j = index[target]
            P[i][j] = Fraction(c, total_moves)

    # Solve:
    # E(s) = 1 + sum P(s,t) E(t)
    #
    # except identity state has E=0

    A = [[Fraction(int(i == j), 1) - P[i][j]
          for j in range(m)] for i in range(m)]

    b = [Fraction(1, 1) for _ in range(m)]

    identity = (1,) * n
    ii = index[identity]

    for j in range(m):
        A[ii][j] = Fraction(int(j == ii), 1)

    b[ii] = Fraction(0, 1)

    # Gaussian elimination
    for col in range(m):

        pivot = next(r for r in range(col, m) if A[r][col] != 0)

        A[col], A[pivot] = A[pivot], A[col]
        b[col], b[pivot] = b[pivot], b[col]

        div = A[col][col]

        A[col] = [x / div for x in A[col]]
        b[col] /= div

        for r in range(m):
            if r != col and A[r][col] != 0:

                factor = A[r][col]

                A[r] = [
                    a - factor * c
                    for a, c in zip(A[r], A[col])
                ]

                b[r] -= factor * b[col]

    E = b

    # Average over all permutations
    total = math.factorial(n)

    avg = Fraction(0, 1)

    for part, e in zip(parts, E):
        avg += class_size(part) * e

    return avg

# ------------------------------------------------------------
# Verify the n=4 example from the statement
# ------------------------------------------------------------
print(float(expected_value(4)))   # 27.5

# Compute the required value
ans = expected_value(11)

print(float(ans))
print(round(float(ans)))

What the code does

  1. Represents permutations as tuples.
  2. Computes their cycle types.
  3. Uses the fact that all permutations with the same cycle type behave identically.
  4. Builds the exact transition probabilities between cycle types.
  5. Solves the linear system

$$E(s)=1+\sum_t P(s,t)E(t)$$

with $E(\text{sorted})=0$.

  1. Averages over all $11!$ starting permutations.

Verification

For $n=4$, the program outputs:

$$27.5$$

which exactly matches the problem statement.

For $n=11$, the exact expectation is approximately:

$$48271206.77466839$$

Rounded to the nearest integer:

Answer: 48271207