Problem 315: Digital Root Clocks

View on Project Euler

Project Euler Problem 315 Solution

EulerSolve provides an optimized solution for Project Euler Problem 315, Digital Root Clocks, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary For each prime $$p\in[10^7,2\cdot 10^7),$$ we form its digital-root chain $$n_0=p,\qquad n_{i+1}=s(n_i),$$ until a single digit is reached. Two seven-segment clocks display this chain: Sam always turns everything off and then lights the next number from scratch. Max only toggles the segments that actually change. We must sum $$\Delta(p)=C_{\text{Sam}}(p)-C_{\text{Max}}(p)$$ over all primes in the interval. Mathematical Approach 1) Seven-segment bitmasks. Each digit \(d\in\{0,\dots,9\}\) is encoded by a 7-bit mask \(M_d\). For example, in the segment convention used by the code, digit \(1\) has 2 lit segments, digit \(7\) has 4, digit \(8\) has 7, and so on. Let $$\sigma(d)=\operatorname{popcount}(M_d)$$ be the number of lit segments in one digit. For a whole number \(n\) with decimal digits \(d_j\), define $$\Sigma(n)=\sum_j \sigma(d_j).$$ This is the cost of lighting \(n\) from a blank display, and also the cost of turning \(n\) fully off. 2) Sam's cost. Sam never tries to reuse lit segments. If the chain is $$n_0\to n_1\to\cdots\to n_t,$$ then each displayed value contributes once to turn it on and once to turn it off. Therefore $$C_{\text{Sam}}(n_0)=2\sum_{i=0}^{t}\Sigma(n_i).$$ 3) Max's cost as a Hamming distance. To go directly from number \(a\) to number \(b\), Max flips only the segments whose states differ....

Detailed mathematical approach

Problem Summary

For each prime

$$p\in[10^7,2\cdot 10^7),$$

we form its digital-root chain

$$n_0=p,\qquad n_{i+1}=s(n_i),$$

until a single digit is reached. Two seven-segment clocks display this chain:

Sam always turns everything off and then lights the next number from scratch.

Max only toggles the segments that actually change.

We must sum

$$\Delta(p)=C_{\text{Sam}}(p)-C_{\text{Max}}(p)$$

over all primes in the interval.

Mathematical Approach

1) Seven-segment bitmasks.

Each digit \(d\in\{0,\dots,9\}\) is encoded by a 7-bit mask \(M_d\). For example, in the segment convention used by the code, digit \(1\) has 2 lit segments, digit \(7\) has 4, digit \(8\) has 7, and so on.

Let

$$\sigma(d)=\operatorname{popcount}(M_d)$$

be the number of lit segments in one digit. For a whole number \(n\) with decimal digits \(d_j\), define

$$\Sigma(n)=\sum_j \sigma(d_j).$$

This is the cost of lighting \(n\) from a blank display, and also the cost of turning \(n\) fully off.

2) Sam's cost.

Sam never tries to reuse lit segments. If the chain is

$$n_0\to n_1\to\cdots\to n_t,$$

then each displayed value contributes once to turn it on and once to turn it off. Therefore

$$C_{\text{Sam}}(n_0)=2\sum_{i=0}^{t}\Sigma(n_i).$$

3) Max's cost as a Hamming distance.

To go directly from number \(a\) to number \(b\), Max flips only the segments whose states differ. Align the numbers by decimal place from the right; missing digits are treated as a blank digit with mask \(0\), not as the numeral \(0\).

Thus the transition cost is

$$D(a,b)=\sum_j \operatorname{popcount}(M_{a_j}\oplus M_{b_j}),$$

where an absent digit contributes mask \(0\).

This is exactly the bitwise Hamming distance between the two displayed numbers.

4) Max's total chain cost.

Max starts from a blank display, walks through the digital-root chain, then returns to blank at the end:

$$C_{\text{Max}}(n_0)=D(0,n_0)+\sum_{i=1}^{t}D(n_{i-1},n_i)+D(n_t,0).$$

Finally, for one prime we add

$$\Delta(n_0)=C_{\text{Sam}}(n_0)-C_{\text{Max}}(n_0).$$

5) Worked example: \(137\to11\to2\).

The chain is

$$137\to 1+3+7=11\to 1+1=2.$$

Segment counts are

$$\Sigma(137)=\sigma(1)+\sigma(3)+\sigma(7)=2+5+4=11,$$

$$\Sigma(11)=2+2=4,\qquad \Sigma(2)=5.$$

So Sam spends

$$C_{\text{Sam}}=2(11+4+5)=40.$$

For Max, the code computes

$$D(0,137)=11,$$

$$D(137,11)=7,$$

$$D(11,2)=7,$$

$$D(2,0)=5.$$

Hence

$$C_{\text{Max}}=11+7+7+5=30,$$

and therefore

$$\Delta(137)=40-30=10.$$

This is exactly the checkpoint embedded in the C++ solution.

6) Why the chain is tiny in this problem.

All primes lie between \(10^7\) and \(2\cdot10^7\), so they have at most 8 digits. The first digit sum is therefore at most

$$1+7\cdot 9=64.$$

After one application of digit sum, the chain is already at most two digits long. After another application it is at most \(10\), and then immediately a single digit. So each prime contributes only a handful of transitions.

7) Prime accumulation.

The rest of the problem is just iteration over primes in the interval. The code uses a sieve to mark all primes in

$$[10^7,2\cdot10^7),$$

builds the digital-root chain for each prime, and accumulates the difference \(\Delta(p)\).

Algorithm

1) Predefine the 7-segment masks for digits \(0\) through \(9\).

2) Sieve the interval to find all primes.

3) For each prime, build the chain \(p\to s(p)\to s(s(p))\to\cdots\).

4) Compute Sam's cost as

$$2\sum \Sigma(n_i).$$

5) Compute Max's cost using transition XOR distances.

6) Add the difference.

Complexity Analysis

The sieve costs

$$O(B\log\log B)$$

time and

$$O(B)$$

memory for upper bound \(B\). After that, each prime contributes only a tiny constant amount of work because its digital-root chain is extremely short in this range.

Checks And Final Result

The implementation checks:

For \(137\), Sam costs \(40\) and Max costs \(30\).

For a single-digit chain such as \(7\), Sam and Max coincide.

The final answer for the full interval is

$$13625242.$$

Further Reading

  1. Problem page: https://projecteuler.net/problem=315
  2. Digital root: https://en.wikipedia.org/wiki/Digital_root
  3. Seven-segment display: https://en.wikipedia.org/wiki/Seven-segment_display

Problem 315 source code

C++

#include <array>
#include <cstdint>
#include <iostream>
#include <string>
#include <vector>

namespace {

using i64 = long long;

struct Options {
    int from = 10000000;
    int to = 20000000;
    bool run_checkpoints = true;
};

bool parse_int_after_prefix(const std::string& arg, const std::string& prefix, int& value) {
    if (arg.rfind(prefix, 0U) != 0U) {
        return false;
    }
    const std::string tail = arg.substr(prefix.size());
    if (tail.empty()) {
        return false;
    }
    int parsed = 0;
    for (char c : tail) {
        if (c < '0' || c > '9') {
            return false;
        }
        parsed = parsed * 10 + static_cast<int>(c - '0');
    }
    value = parsed;
    return true;
}

bool parse_arguments(int argc, char** argv, Options& options) {
    for (int i = 1; i < argc; ++i) {
        const std::string arg(argv[i]);
        if (arg == "--skip-checkpoints") {
            options.run_checkpoints = false;
            continue;
        }
        if (parse_int_after_prefix(arg, "--from=", options.from) ||
            parse_int_after_prefix(arg, "--to=", options.to)) {
            continue;
        }
        std::cerr << "Unknown argument: " << arg << '\n';
        return false;
    }
    return options.from >= 2 && options.to > options.from;
}

std::array<int, 10> digit_masks() {
    // Segment order: top, middle, bottom, top-left, top-right, bottom-left, bottom-right.
    constexpr int T = 1 << 0;
    constexpr int M = 1 << 1;
    constexpr int B = 1 << 2;
    constexpr int TL = 1 << 3;
    constexpr int TR = 1 << 4;
    constexpr int BL = 1 << 5;
    constexpr int BR = 1 << 6;

    return {
        T | B | TL | TR | BL | BR,          // 0
        TR | BR,                            // 1
        T | M | B | TR | BL,                // 2
        T | M | B | TR | BR,                // 3
        M | TL | TR | BR,                   // 4
        T | M | B | TL | BR,                // 5
        T | M | B | TL | BL | BR,           // 6
        T | TL | TR | BR,                   // 7 (4 segments in this display style)
        T | M | B | TL | TR | BL | BR,      // 8
        T | M | B | TL | TR | BR            // 9
    };
}

int digit_sum(int n) {
    int s = 0;
    while (n > 0) {
        s += n % 10;
        n /= 10;
    }
    return s;
}

std::vector<int> digital_root_chain(int n) {
    std::vector<int> chain;
    while (true) {
        chain.push_back(n);
        if (n < 10) {
            break;
        }
        n = digit_sum(n);
    }
    return chain;
}

int segment_count_number(int n, const std::array<int, 10>& masks) {
    int total = 0;
    while (n > 0) {
        total += __builtin_popcount(static_cast<unsigned>(masks[static_cast<std::size_t>(n % 10)]));
        n /= 10;
    }
    return total;
}

int transition_cost(int from, int to, const std::array<int, 10>& masks) {
    int cost = 0;
    while (from > 0 || to > 0) {
        const int a = (from > 0) ? masks[static_cast<std::size_t>(from % 10)] : 0;
        const int b = (to > 0) ? masks[static_cast<std::size_t>(to % 10)] : 0;
        cost += __builtin_popcount(static_cast<unsigned>(a ^ b));
        from /= 10;
        to /= 10;
    }
    return cost;
}

int sam_cost(const std::vector<int>& chain, const std::array<int, 10>& masks) {
    int cost = 0;
    for (int value : chain) {
        cost += 2 * segment_count_number(value, masks);
    }
    return cost;
}

int max_cost(const std::vector<int>& chain, const std::array<int, 10>& masks) {
    int cost = 0;
    cost += transition_cost(0, chain.front(), masks);
    for (std::size_t i = 1; i < chain.size(); ++i) {
        cost += transition_cost(chain[i - 1], chain[i], masks);
    }
    cost += transition_cost(chain.back(), 0, masks);
    return cost;
}

i64 solve(const int from, const int to) {
    const auto masks = digit_masks();

    std::vector<std::uint8_t> is_prime(static_cast<std::size_t>(to), 1U);
    is_prime[0] = 0U;
    if (to > 1) {
        is_prime[1] = 0U;
    }
    for (int p = 2; static_cast<long long>(p) * p < to; ++p) {
        if (is_prime[static_cast<std::size_t>(p)] == 0U) {
            continue;
        }
        for (int q = p * p; q < to; q += p) {
            is_prime[static_cast<std::size_t>(q)] = 0U;
        }
    }

    i64 total_diff = 0;
    for (int p = from; p < to; ++p) {
        if (is_prime[static_cast<std::size_t>(p)] == 0U) {
            continue;
        }
        const auto chain = digital_root_chain(p);
        total_diff += static_cast<i64>(sam_cost(chain, masks) - max_cost(chain, masks));
    }
    return total_diff;
}

bool run_checkpoints() {
    const auto masks = digit_masks();
    {
        const auto chain = digital_root_chain(137);
        const int sam = sam_cost(chain, masks);
        const int mx = max_cost(chain, masks);
        if (sam != 40 || mx != 30) {
            std::cerr << "Checkpoint failed for 137 sample transitions" << '\n';
            return false;
        }
    }
    {
        const auto chain = digital_root_chain(7);
        if (sam_cost(chain, masks) != max_cost(chain, masks)) {
            std::cerr << "Checkpoint failed for single-digit transition equivalence" << '\n';
            return false;
        }
    }
    return true;
}

}  // namespace

int main(int argc, char** argv) {
    Options options;
    if (!parse_arguments(argc, argv, options)) {
        return 1;
    }
    if (options.run_checkpoints && !run_checkpoints()) {
        return 2;
    }
    std::cout << solve(options.from, options.to) << '\n';
    return 0;
}

Python

def solve():
    FROM = 10000000
    TO = 20000000

    # Segment masks: T, M, B, TL, TR, BL, BR
    masks = [
        0b1111101,  # 0: T|B|TL|TR|BL|BR
        0b1010000,  # 1: TR|BR
        0b0110110,  # 2: T|M|B|TR|BL
        0b1110100,  # 3: T|M|B|TR|BR
        0b1011010,  # 4: M|TL|TR|BR
        0b1101100,  # 5: T|M|B|TL|BR
        0b1101110,  # 6: T|M|B|TL|BL|BR
        0b1011001,  # 7: T|TL|TR|BR (4-seg style)
        0b1111111,  # 8: all
        0b1111100,  # 9: T|M|B|TL|TR|BR
    ]

    # Wait, let me re-derive from the C++ code carefully
    T = 1; M = 2; B = 4; TL = 8; TR = 16; BL = 32; BR = 64
    masks = [
        T|B|TL|TR|BL|BR,       # 0
        TR|BR,                  # 1
        T|M|B|TR|BL,           # 2
        T|M|B|TR|BR,           # 3
        M|TL|TR|BR,            # 4
        T|M|B|TL|BR,           # 5
        T|M|B|TL|BL|BR,       # 6
        T|TL|TR|BR,            # 7
        T|M|B|TL|TR|BL|BR,    # 8
        T|M|B|TL|TR|BR,       # 9
    ]

    def popcount(x):
        return bin(x).count('1')

    def digit_sum(n):
        s = 0
        while n > 0:
            s += n % 10
            n //= 10
        return s

    def digital_root_chain(n):
        chain = []
        while True:
            chain.append(n)
            if n < 10:
                break
            n = digit_sum(n)
        return chain

    def seg_count(n):
        total = 0
        while n > 0:
            total += popcount(masks[n % 10])
            n //= 10
        return total

    def transition_cost(fr, to):
        cost = 0
        while fr > 0 or to > 0:
            a = masks[fr % 10] if fr > 0 else 0
            b = masks[to % 10] if to > 0 else 0
            cost += popcount(a ^ b)
            fr //= 10
            to //= 10
        return cost

    def sam_cost(chain):
        return sum(2 * seg_count(v) for v in chain)

    def max_cost(chain):
        cost = transition_cost(0, chain[0])
        for i in range(1, len(chain)):
            cost += transition_cost(chain[i-1], chain[i])
        cost += transition_cost(chain[-1], 0)
        return cost

    # Sieve primes
    is_prime = bytearray(b'\x01' * TO)
    is_prime[0] = 0
    is_prime[1] = 0
    p = 2
    while p * p < TO:
        if is_prime[p]:
            is_prime[p*p::p] = bytearray(len(is_prime[p*p::p]))
        p += 1

    total_diff = 0
    for p in range(FROM, TO):
        if not is_prime[p]:
            continue
        chain = digital_root_chain(p)
        total_diff += sam_cost(chain) - max_cost(chain)

    return str(total_diff)

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

Java

public class Euler315 {
    static int[] digitMasks() {
        int T = 1 << 0;
        int M = 1 << 1;
        int B = 1 << 2;
        int TL = 1 << 3;
        int TR = 1 << 4;
        int BL = 1 << 5;
        int BR = 1 << 6;

        return new int[] {
                T | B | TL | TR | BL | BR,
                TR | BR,
                T | M | B | TR | BL,
                T | M | B | TR | BR,
                M | TL | TR | BR,
                T | M | B | TL | BR,
                T | M | B | TL | BL | BR,
                T | TL | TR | BR,
                T | M | B | TL | TR | BL | BR,
                T | M | B | TL | TR | BR
        };
    }

    static int digitSum(int n) {
        int s = 0;
        while (n > 0) {
            s += n % 10;
            n /= 10;
        }
        return s;
    }

    static int segmentCountNumber(int n, int[] masks) {
        int total = 0;
        while (n > 0) {
            total += Integer.bitCount(masks[n % 10]);
            n /= 10;
        }
        return total;
    }

    static int transitionCost(int from, int to, int[] masks) {
        int cost = 0;
        while (from > 0 || to > 0) {
            int a = (from > 0) ? masks[from % 10] : 0;
            int b = (to > 0) ? masks[to % 10] : 0;
            cost += Integer.bitCount(a ^ b);
            from /= 10;
            to /= 10;
        }
        return cost;
    }

    public static String solve() {
        int from = 10000000;
        int to = 20000000;
        int[] masks = digitMasks();

        byte[] isPrime = new byte[to];
        for (int i = 2; i < to; i++)
            isPrime[i] = 1;
        for (int p = 2; (long) p * p < to; ++p) {
            if (isPrime[p] == 1) {
                for (int q = p * p; q < to; q += p) {
                    isPrime[q] = 0;
                }
            }
        }

        long totalDiff = 0;
        int[] chain = new int[16];

        for (int p = from; p < to; ++p) {
            if (isPrime[p] == 0)
                continue;

            int cSize = 0;
            int curr = p;
            while (true) {
                chain[cSize++] = curr;
                if (curr < 10)
                    break;
                curr = digitSum(curr);
            }

            int samCost = 0;
            for (int i = 0; i < cSize; i++) {
                samCost += 2 * segmentCountNumber(chain[i], masks);
            }

            int mxCost = transitionCost(0, chain[0], masks);
            for (int i = 1; i < cSize; i++) {
                mxCost += transitionCost(chain[i - 1], chain[i], masks);
            }
            mxCost += transitionCost(chain[cSize - 1], 0, masks);

            totalDiff += (samCost - mxCost);
        }

        return String.valueOf(totalDiff);
    }

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