Problem 891: Ambiguous Clock

View on Project Euler

Project Euler Problem 891 Solution

EulerSolve provides an optimized solution for Project Euler Problem 891, Ambiguous Clock, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary Let \(T=43200\) denote one full 12-hour cycle measured in seconds. If the hour, minute, and second hands are modeled by the speed triple $$s_1=1,\qquad s_2=12,\qquad s_3=720,$$ then at time \(t\in[0,T)\) their angular positions are \(s_1t\), \(s_2t\), and \(s_3t\) modulo \(T\). The task is to count the moments for which the visible three-point pattern on the dial is ambiguous: the same observed configuration can be explained by at least two different assignments of the labels “hour”, “minute”, and “second”. Mathematical Approach The C++, Python, and Java implementations solve the problem one permutation at a time. For each non-identity relabeling, the geometry becomes a pair of linear congruences, which then reduces to a finite rational grid of candidate times. Step 1: Write the configuration equations up to a common rotation Suppose that the configuration seen at time \(t\) can also be explained by another time \(\tau\) after a nontrivial permutation \(\pi\) of the three hand labels. Because the observed pattern is unlabeled, the two descriptions may differ by a global rotation \(r\). Therefore $$s_i t \equiv s_{\pi(i)}\tau + r \pmod{T},\qquad i=1,2,3.$$ Subtract the first equation from the second and third to eliminate the unknown rotation....

Detailed mathematical approach

Problem Summary

Let \(T=43200\) denote one full 12-hour cycle measured in seconds. If the hour, minute, and second hands are modeled by the speed triple

$$s_1=1,\qquad s_2=12,\qquad s_3=720,$$

then at time \(t\in[0,T)\) their angular positions are \(s_1t\), \(s_2t\), and \(s_3t\) modulo \(T\). The task is to count the moments for which the visible three-point pattern on the dial is ambiguous: the same observed configuration can be explained by at least two different assignments of the labels “hour”, “minute”, and “second”.

Mathematical Approach

The C++, Python, and Java implementations solve the problem one permutation at a time. For each non-identity relabeling, the geometry becomes a pair of linear congruences, which then reduces to a finite rational grid of candidate times.

Step 1: Write the configuration equations up to a common rotation

Suppose that the configuration seen at time \(t\) can also be explained by another time \(\tau\) after a nontrivial permutation \(\pi\) of the three hand labels. Because the observed pattern is unlabeled, the two descriptions may differ by a global rotation \(r\). Therefore

$$s_i t \equiv s_{\pi(i)}\tau + r \pmod{T},\qquad i=1,2,3.$$

Subtract the first equation from the second and third to eliminate the unknown rotation. Since

$$s_2-s_1=11,\qquad s_3-s_1=719,$$

we obtain

$$11t \equiv \alpha_{\pi}\tau \pmod{T},\qquad 719t \equiv \beta_{\pi}\tau \pmod{T},$$

where

$$\alpha_{\pi}=s_{\pi(2)}-s_{\pi(1)},\qquad \beta_{\pi}=s_{\pi(3)}-s_{\pi(1)}.$$

At this point the clock picture has been converted into pure modular arithmetic.

Step 2: Eliminate the alternative time and get a discrete time grid

Multiply the first congruence by \(\beta_{\pi}\) and the second by \(\alpha_{\pi}\), then subtract. The term containing \(\tau\) disappears:

$$\left(11\beta_{\pi}-719\alpha_{\pi}\right)t \equiv 0 \pmod{T}.$$

Define the determinant-like quantity

$$\Delta_{\pi}=719\alpha_{\pi}-11\beta_{\pi},\qquad N_{\pi}=|\Delta_{\pi}|.$$

Then every candidate moment for that permutation must satisfy

$$\Delta_{\pi}t \equiv 0 \pmod{T},$$

so the solutions in \([0,T)\) are exactly

$$t_n=\frac{Tn}{N_{\pi}},\qquad n=0,1,\dots,N_{\pi}-1.$$

Thus each permutation contributes a finite arithmetic grid of rational times rather than a continuous search over the interval.

Step 3: Recover the second interpretation with Bézout coefficients

For every non-identity permutation in this problem, the pair \((\alpha_{\pi},\beta_{\pi})\) is coprime. Hence there exist integers \(u_{\pi},v_{\pi}\) such that

$$u_{\pi}\beta_{\pi}+v_{\pi}\alpha_{\pi}=1.$$

Multiply the congruence \(11t\equiv \alpha_{\pi}\tau\pmod{T}\) by \(v_{\pi}\) and the congruence \(719t\equiv \beta_{\pi}\tau\pmod{T}\) by \(u_{\pi}\), then add them. The left-hand side becomes \(\tau\):

$$\tau \equiv \lambda_{\pi} t \pmod{T},\qquad \lambda_{\pi}=11v_{\pi}+719u_{\pi}.$$

Because \(t=t_n=Tn/N_{\pi}\), the partner time also lies on the same grid:

$$\tau=t_{n'},\qquad n' \equiv \lambda_{\pi}n \pmod{N_{\pi}}.$$

A moment is genuinely ambiguous for that permutation exactly when \(n'\ne n\). Fixed points are ignored because they do not produce a second distinct interpretation.

Step 4: Reduce every candidate to one canonical rational time

The same real moment can arise from different permutations, so the times must be deduplicated globally. For one permutation we start from

$$t_n=\frac{Tn}{N_{\pi}}.$$

Let

$$g_0=\gcd(T,N_{\pi}),\qquad T_0=\frac{T}{g_0},\qquad N_0=\frac{N_{\pi}}{g_0}.$$

Then

$$t_n=\frac{T_0 n}{N_0}.$$

Reducing once more by \(g=\gcd(n,N_0)\) gives the canonical fraction

$$t_n=\frac{T_0(n/g)}{N_0/g}.$$

Storing the pair \(\left(T_0(n/g),\,N_0/g\right)\) guarantees that equal moments coming from different permutations are merged correctly.

Step 5: Worked example

Take the permutation that swaps the hour and minute roles while keeping the fastest hand in the third position, so that

$$\left(s_{\pi(1)},s_{\pi(2)},s_{\pi(3)}\right)=(12,1,720).$$

Then

$$\alpha_{\pi}=1-12=-11,\qquad \beta_{\pi}=720-12=708,$$

and therefore

$$\Delta_{\pi}=719(-11)-11(708)=-15697,\qquad N_{\pi}=15697.$$

One Bézout identity is

$$3\cdot 708 + 193\cdot(-11)=1,$$

so we may take \(u_{\pi}=3\) and \(v_{\pi}=193\). This yields

$$\lambda_{\pi}\equiv 11\cdot 193 + 719\cdot 3 \equiv 4280 \pmod{15697}.$$

Hence the candidate times are

$$t_n=\frac{43200n}{15697},\qquad \tau_n=\frac{43200\left(4280n\bmod 15697\right)}{15697}.$$

For \(n=1\) we get

$$t=\frac{43200}{15697}\approx 2.752118239\text{ s},\qquad \tau=\frac{43200\cdot 4280}{15697}\approx 11779.066063579\text{ s}.$$

Because \(4280\not\equiv 1\pmod{15697}\), this is a genuine ambiguous moment. The implementations repeat exactly this calculation for all five non-identity permutations and then remove overlaps.

How the Code Works

The C++, Python, and Java implementations iterate over the five non-identity permutations of the speed triple \((1,12,720)\). For each permutation they compute the two speed differences, the determinant magnitude \(N_{\pi}\), and one Bézout pair that reconstructs the partner index \(n'\) from \(n\).

They then scan every \(n\) with \(0\le n<N_{\pi}\), discard the fixed points satisfying \(n'=n\), and convert the surviving time \(Tn/N_{\pi}\) into a reduced numerator-denominator pair. This reduction is purely arithmetic, so no floating-point comparison is needed.

The C++ implementation collects all reduced fractions and performs a final sort-and-unique pass. The Python and Java implementations deduplicate immediately with set-style storage. Despite that storage difference, all three implementations enumerate the same rational moments and return the size of the final unique collection.

Complexity Analysis

Let \(N_{\pi}=|719\alpha_{\pi}-11\beta_{\pi}|\). The main work is the enumeration of all indices \(n=0,\dots,N_{\pi}-1\) over the five non-identity permutations, so the arithmetic scan costs

$$O\!\left(\sum_{\pi\ne\mathrm{id}} N_{\pi}\right).$$

Each candidate requires only a constant amount of gcd arithmetic. If \(M\) candidate fractions survive the fixed-point test, the final deduplication cost is \(O(M\log M)\) for the sort-based implementation and expected \(O(M)\) for the set-based ones. Memory usage is \(O(M)\). For this concrete speed triple, the total scan length is \(2052026\), so the method is easily practical.

Footnotes and References

  1. Problem page: Project Euler 891
  2. Modular arithmetic: Wikipedia - Modular arithmetic
  3. Bézout's identity: Wikipedia - Bézout's identity
  4. Extended Euclidean algorithm: Wikipedia - Extended Euclidean algorithm
  5. Permutations: Wikipedia - Permutation

Problem 891 source code

C++

#include <algorithm>
#include <array>
#include <cstdint>
#include <iostream>
#include <numeric>
#include <utility>
#include <vector>

namespace {

constexpr std::int64_t kCycleSeconds = 43200;

std::int64_t extended_gcd(std::int64_t a, std::int64_t b, std::int64_t& x,
                          std::int64_t& y) {
    if (b == 0) {
        x = 1;
        y = 0;
        return a;
    }
    std::int64_t x1 = 0;
    std::int64_t y1 = 0;
    std::int64_t g = extended_gcd(b, a % b, x1, y1);
    x = y1;
    y = x1 - (a / b) * y1;
    return g;
}

}  // namespace

int main() {
    const std::int64_t speeds[3] = {1, 12, 720};
    std::array<int, 3> idx = {0, 1, 2};

    std::vector<std::pair<std::int64_t, std::int64_t>> moments;
    moments.reserve(2100000);

    // For each non-identity permutation, eliminate rotation via differences to
    // get a linear congruence system: t = 43200*n/|det| and n' = C*n (mod |det|).
    do {
        if (idx[0] == 0 && idx[1] == 1 && idx[2] == 2) {
            continue;
        }
        std::int64_t pa = speeds[idx[0]];
        std::int64_t pb = speeds[idx[1]];
        std::int64_t pc = speeds[idx[2]];

        std::int64_t A = pb - pa;
        std::int64_t B = pc - pa;
        std::int64_t det = 719 * A - 11 * B;
        if (det == 0) {
            continue;
        }
        std::int64_t D = det > 0 ? det : -det;

        std::int64_t absA = A >= 0 ? A : -A;
        std::int64_t absB = B >= 0 ? B : -B;
        std::int64_t x = 0;
        std::int64_t y = 0;
        std::int64_t g = extended_gcd(absB, absA, x, y);
        if (g != 1) {
            std::cerr << "Unexpected gcd value.\n";
            return 1;
        }
        std::int64_t u = x * (B < 0 ? -1 : 1);
        std::int64_t v = y * (A < 0 ? -1 : 1);

        std::int64_t C = (11 * v + 719 * u) % D;
        if (C < 0) {
            C += D;
        }

        std::int64_t g1 = std::gcd<std::int64_t>(kCycleSeconds, D);
        std::int64_t num_base = kCycleSeconds / g1;
        std::int64_t den_base = D / g1;

        for (std::int64_t n = 0; n < D; ++n) {
            std::int64_t n2 = static_cast<std::int64_t>((__int128)n * C % D);
            if (n2 == n) {
                continue;
            }
            std::int64_t g2 = std::gcd<std::int64_t>(n, den_base);
            std::int64_t num = num_base * (n / g2);
            std::int64_t den = den_base / g2;
            moments.emplace_back(num, den);
        }
    } while (std::next_permutation(idx.begin(), idx.end()));

    std::sort(moments.begin(), moments.end());
    moments.erase(std::unique(moments.begin(), moments.end()), moments.end());

    std::cout << moments.size() << '\n';
    return 0;
}

Python

import math

def solve():
    CYCLE = 43200; speeds = [1, 12, 720]
    from itertools import permutations

    def egcd(a, b):
        if b == 0: return a, 1, 0
        g, x1, y1 = egcd(b, a%b)
        return g, y1, x1-(a//b)*y1

    moments = set()
    for perm in permutations([0,1,2]):
        if list(perm) == [0,1,2]: continue
        pa, pb, pc = speeds[perm[0]], speeds[perm[1]], speeds[perm[2]]
        A = pb-pa; B = pc-pa; det = 719*A - 11*B
        if det == 0: continue
        D = abs(det)
        absA, absB = abs(A), abs(B)
        g, x, y = egcd(absB, absA)
        u = x*(1 if B>=0 else -1); v = y*(1 if A>=0 else -1)
        C = (11*v + 719*u) % D
        if C < 0: C += D
        g1 = math.gcd(CYCLE, D); nb = CYCLE//g1; db = D//g1
        for n in range(D):
            n2 = n*C%D
            if n2 == n: continue
            g2 = math.gcd(n, db)
            num = nb*(n//g2); den = db//g2
            moments.add((num, den))

    return str(len(moments))

if __name__ == '__main__':
    print(solve())

Java

import java.util.HashSet;
import java.util.Objects;

public class Euler891 {
    static final long kCycleSeconds = 43200;

    static class GCDResult {
        long g, x, y;

        GCDResult(long g, long x, long y) {
            this.g = g;
            this.x = x;
            this.y = y;
        }
    }

    static GCDResult extendedGCD(long a, long b) {
        if (b == 0) {
            return new GCDResult(a, 1, 0);
        }
        GCDResult res = extendedGCD(b, a % b);
        long x = res.y;
        long y = res.x - (a / b) * res.y;
        return new GCDResult(res.g, x, y);
    }

    static long gcd(long a, long b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    static class Fraction {
        long num, den;

        Fraction(long num, long den) {
            this.num = num;
            this.den = den;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;
            Fraction fraction = (Fraction) o;
            return num == fraction.num && den == fraction.den;
        }

        @Override
        public int hashCode() {
            return Objects.hash(num, den);
        }
    }

    public static String solve() {
        long[] speeds = { 1, 12, 720 };
        int[][] permutations = {
                { 0, 2, 1 }, { 1, 0, 2 }, { 1, 2, 0 }, { 2, 0, 1 }, { 2, 1, 0 }
        };

        HashSet<Fraction> moments = new HashSet<>();

        for (int[] idx : permutations) {
            long pa = speeds[idx[0]];
            long pb = speeds[idx[1]];
            long pc = speeds[idx[2]];

            long A = pb - pa;
            long B = pc - pa;
            long det = 719 * A - 11 * B;
            if (det == 0)
                continue;

            long D = Math.abs(det);
            long absA = Math.abs(A);
            long absB = Math.abs(B);

            GCDResult res = extendedGCD(absB, absA);
            long u = res.x * (B < 0 ? -1 : 1);
            long v = res.y * (A < 0 ? -1 : 1);

            long C = (11 * v + 719 * u) % D;
            if (C < 0)
                C += D;

            long g1 = gcd(kCycleSeconds, D);
            long numBase = kCycleSeconds / g1;
            long denBase = D / g1;

            for (long n = 0; n < D; ++n) {
                long n2 = (n * C) % D;
                if (n2 == n)
                    continue;

                long g2 = gcd(n, denBase);
                long num = numBase * (n / g2);
                long den = denBase / g2;
                moments.add(new Fraction(num, den));
            }
        }

        return Integer.toString(moments.size());
    }

    public static void main(String[] args) {
        System.out.println(solve());
    }
}