Problem 776: Digit Sum Division

View on Project Euler

Project Euler Problem 776 Solution

EulerSolve provides an optimized solution for Project Euler Problem 776, Digit Sum Division, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary We must evaluate $$F(N)=\sum_{n=1}^{N}\frac{n}{s(n)},$$ where \(s(n)\) is the sum of the decimal digits of \(n\). The target bound is a 19-digit number, so iterating over every \(n\le N\) is infeasible. The crucial observation is that the denominator depends only on the digit sum, so the whole sum can be reorganized by digit-sum classes and computed with digit dynamic programming. Mathematical Approach Let \(L\) be the number of decimal digits of \(N\). Any \(L\)-digit decimal string has digit sum between \(0\) and \(9L\), so there are only \(9L+1\) relevant classes. The implementation uses that small state space to aggregate many numbers at once. Step 1: Group the sum by digit sum Define $$A_\sigma(N)=\sum_{\substack{1\le n\le N\\ s(n)=\sigma}} n.$$ Then the original expression becomes $$F(N)=\sum_{\sigma=1}^{9L}\frac{A_\sigma(N)}{\sigma}.$$ So we do not need to handle each value \(n/s(n)\) separately. It is enough to know, for each possible digit sum \(\sigma\), the total of all numbers up to \(N\) whose digit sum is exactly \(\sigma\). Step 2: Scan the decimal expansion from left to right Write the bound as decimal digits \(d_1d_2\dots d_L\). We process these digits from left to right and allow leading zeros, so every integer \(0\le n\le N\) is represented exactly once as an \(L\)-digit string....

Detailed mathematical approach

Problem Summary

We must evaluate

$$F(N)=\sum_{n=1}^{N}\frac{n}{s(n)},$$

where \(s(n)\) is the sum of the decimal digits of \(n\). The target bound is a 19-digit number, so iterating over every \(n\le N\) is infeasible. The crucial observation is that the denominator depends only on the digit sum, so the whole sum can be reorganized by digit-sum classes and computed with digit dynamic programming.

Mathematical Approach

Let \(L\) be the number of decimal digits of \(N\). Any \(L\)-digit decimal string has digit sum between \(0\) and \(9L\), so there are only \(9L+1\) relevant classes. The implementation uses that small state space to aggregate many numbers at once.

Step 1: Group the sum by digit sum

Define

$$A_\sigma(N)=\sum_{\substack{1\le n\le N\\ s(n)=\sigma}} n.$$

Then the original expression becomes

$$F(N)=\sum_{\sigma=1}^{9L}\frac{A_\sigma(N)}{\sigma}.$$

So we do not need to handle each value \(n/s(n)\) separately. It is enough to know, for each possible digit sum \(\sigma\), the total of all numbers up to \(N\) whose digit sum is exactly \(\sigma\).

Step 2: Scan the decimal expansion from left to right

Write the bound as decimal digits \(d_1d_2\dots d_L\). We process these digits from left to right and allow leading zeros, so every integer \(0\le n\le N\) is represented exactly once as an \(L\)-digit string.

After processing the first \(k\) positions, for every digit sum \(\sigma\) we store four quantities:

$$C_k^{\mathrm{eq}}(\sigma),\quad S_k^{\mathrm{eq}}(\sigma),\qquad C_k^{\mathrm{lt}}(\sigma),\quad S_k^{\mathrm{lt}}(\sigma).$$

The \(C\) values count how many prefixes lie in that state, and the \(S\) values store the sum of the numeric values of those prefixes. The superscript \(\mathrm{eq}\) means the processed prefix is exactly equal to the first \(k\) digits of \(N\); the superscript \(\mathrm{lt}\) means it is already smaller, so the remaining digits are unrestricted.

Initially only the empty prefix exists:

$$C_0^{\mathrm{eq}}(0)=1,\qquad S_0^{\mathrm{eq}}(0)=0,$$

and every other state is zero.

Step 3: Update both the count and the numeric total

Suppose a state currently contains \(C\) prefixes with digit sum \(\sigma\) and total numeric sum \(S\). Appending a digit \(d\) transforms every old value \(x\) into \(10x+d\). Therefore the new digit sum is \(\sigma+d\), the number of resulting prefixes is still \(C\), and their total numeric sum is

$$\sum (10x+d)=10S+dC.$$

So one transition contributes

$$\Delta C=C,\qquad \Delta S=10S+dC.$$

If the current state is exact, then appending the current limit digit stays in the exact family, while appending a smaller digit moves to the smaller family. If the current state is already smaller, then any digit \(0,\dots,9\) remains in the smaller family.

A tiny example shows why the formula is correct. If a state contains the prefixes \(1\) and \(4\), then \(C=2\) and \(S=5\). Appending digit \(7\) produces \(17\) and \(47\), whose total is \(64\). The formula gives the same result immediately:

$$10S+dC=10\cdot 5+7\cdot 2=64.$$

Step 4: Recover the final sum from the last layer

After all \(L\) digits are processed, the total numeric sum of all values in \(0\le n\le N\) with digit sum \(\sigma\) is

$$A_\sigma(N)=S_L^{\mathrm{eq}}(\sigma)+S_L^{\mathrm{lt}}(\sigma).$$

Leading zeros automatically include shorter numbers. The class \(\sigma=0\) contains only the number \(0\), and it must be excluded because the denominator would be zero. Hence

$$F(N)=\sum_{\sigma=1}^{9L}\frac{S_L^{\mathrm{eq}}(\sigma)+S_L^{\mathrm{lt}}(\sigma)}{\sigma}.$$

Worked Example: \(N=10\)

Using two-digit strings, the integers from \(0\) to \(10\) are \(00,01,\dots,09,10\). Group the positive ones by digit sum:

$$\sigma=1:\ \{1,10\},\qquad A_1(10)=11,$$

$$\sigma=2:\ \{2\},\ A_2(10)=2,\qquad \dots,\qquad \sigma=9:\ \{9\},\ A_9(10)=9.$$

Therefore

$$F(10)=\frac{11}{1}+\frac{2}{2}+\frac{3}{3}+\cdots+\frac{9}{9}=11+8=19.$$

This matches the checkpoint used by the implementations and shows exactly why grouping by digit sum is sufficient.

How the Code Works

The C++, Python, and Java implementations all follow the same plan. They convert the bound to a decimal string, compute \(L\) and the maximum digit sum \(9L\), and maintain two rolling tables indexed by digit sum: one for prefixes still equal to the bound and one for prefixes already below it.

Each table entry stores two exact quantities: how many prefixes are represented there, and the sum of those prefix values as integers. At every decimal position, the implementation creates fresh tables, loops over the reachable digit sums, and applies the append-a-digit rule \(10S+dC\) to build the next layer.

After the last digit, the implementation combines the exact-prefix and smaller-prefix totals for every positive digit sum \(\sigma\), divides by \(\sigma\), and accumulates the final decimal answer. Exact integer accumulation is essential because the intermediate class sums grow far beyond machine-word size, while high-precision decimal arithmetic is used only for the final divisions and output formatting.

Complexity Analysis

There are \(L\) digit positions, \(9L+1\) possible digit sums, and at most \(10\) candidate digits per transition. Therefore the running time is

$$O(L\cdot 9L\cdot 10)=O(L^2).$$

The rolling tables use \(O(9L)=O(L)\) memory. Since \(L=\Theta(\log_{10} N)\), this is \(O((\log N)^2)\) time and \(O(\log N)\) memory as a function of the size of the bound.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=776
  2. Digital sum: Wikipedia — Digital sum
  3. Dynamic programming: Wikipedia — Dynamic programming
  4. Positional notation: Wikipedia — Positional notation
  5. Arbitrary-precision arithmetic: Wikipedia — Arbitrary-precision arithmetic

Problem 776 source code

C++

#include <cassert>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

#include <boost/multiprecision/cpp_dec_float.hpp>
#include <boost/multiprecision/cpp_int.hpp>

namespace {

using boost::multiprecision::cpp_dec_float_100;
using boost::multiprecision::cpp_int;

struct Stats {
    cpp_int count = 0;
    cpp_int sum = 0;
};

cpp_dec_float_100 F_of_u64(std::uint64_t N) {
    const std::string s = std::to_string(N);
    const int L = static_cast<int>(s.size());
    const int MAX_SUM = 9 * L;

    std::vector<Stats> tight(MAX_SUM + 1), loose(MAX_SUM + 1);
    tight[0].count = 1;

    for (int pos = 0; pos < L; ++pos) {
        const int limit = s[static_cast<std::size_t>(pos)] - '0';
        std::vector<Stats> ntight(MAX_SUM + 1), nloose(MAX_SUM + 1);

        for (int sum = 0; sum <= MAX_SUM; ++sum) {
            if (tight[sum].count != 0) {
                for (int d = 0; d <= limit; ++d) {
                    const int ns = sum + d;
                    Stats& dst = (d == limit) ? ntight[ns] : nloose[ns];
                    dst.count += tight[sum].count;
                    dst.sum += tight[sum].sum * 10 + tight[sum].count * d;
                }
            }

            if (loose[sum].count != 0) {
                for (int d = 0; d <= 9; ++d) {
                    const int ns = sum + d;
                    Stats& dst = nloose[ns];
                    dst.count += loose[sum].count;
                    dst.sum += loose[sum].sum * 10 + loose[sum].count * d;
                }
            }
        }

        tight.swap(ntight);
        loose.swap(nloose);
    }

    cpp_dec_float_100 ans = 0;
    for (int sum = 1; sum <= MAX_SUM; ++sum) {
        const cpp_int total_sum = tight[sum].sum + loose[sum].sum;
        if (total_sum == 0) {
            continue;
        }
        ans += cpp_dec_float_100(total_sum) / cpp_dec_float_100(sum);
    }
    return ans;
}

void assert_close(const cpp_dec_float_100& got, const cpp_dec_float_100& expected,
                  const cpp_dec_float_100& tol) {
    cpp_dec_float_100 diff = got - expected;
    if (diff < 0) {
        diff = -diff;
    }
    assert(diff <= tol);
}

}  // namespace

int main() {
    assert(F_of_u64(10) == cpp_dec_float_100(19));
    assert_close(F_of_u64(123), cpp_dec_float_100("1.187764610390e3"),
                 cpp_dec_float_100("1e-9"));
    assert_close(F_of_u64(12345), cpp_dec_float_100("4.855801996238e6"),
                 cpp_dec_float_100("1e-6"));

    const cpp_dec_float_100 ans = F_of_u64(1'234'567'890'123'456'789ULL);
    std::cout << std::scientific << std::setprecision(12) << ans << '\n';
    return 0;
}

Python

import decimal

def f_of_u64(N):
    decimal.getcontext().prec = 100
    s = str(N)
    L = len(s)
    max_sum = 9 * L
    
    tight = [{'count': 0, 'sum': 0} for _ in range(max_sum + 1)]
    loose = [{'count': 0, 'sum': 0} for _ in range(max_sum + 1)]
    
    tight[0]['count'] = 1
    
    for pos in range(L):
        limit = int(s[pos])
        ntight = [{'count': 0, 'sum': 0} for _ in range(max_sum + 1)]
        nloose = [{'count': 0, 'sum': 0} for _ in range(max_sum + 1)]
        
        for sum_val in range(max_sum + 1):
            t_cnt = tight[sum_val]['count']
            t_sum = tight[sum_val]['sum']
            if t_cnt != 0:
                for d in range(limit + 1):
                    ns = sum_val + d
                    if d == limit:
                        ntight[ns]['count'] += t_cnt
                        ntight[ns]['sum'] += t_sum * 10 + t_cnt * d
                    else:
                        nloose[ns]['count'] += t_cnt
                        nloose[ns]['sum'] += t_sum * 10 + t_cnt * d
                        
            l_cnt = loose[sum_val]['count']
            l_sum = loose[sum_val]['sum']
            if l_cnt != 0:
                for d in range(10):
                    ns = sum_val + d
                    nloose[ns]['count'] += l_cnt
                    nloose[ns]['sum'] += l_sum * 10 + l_cnt * d
                    
        tight = ntight
        loose = nloose
        
    ans = decimal.Decimal(0)
    for sum_val in range(1, max_sum + 1):
        total_sum = tight[sum_val]['sum'] + loose[sum_val]['sum']
        if total_sum == 0:
            continue
        ans += decimal.Decimal(total_sum) / decimal.Decimal(sum_val)
        
    # Return formatted as e.g. 4.855801996238e+06
    # Note that scientific format with 12 decimals exactly matches C++'s setprecision(12)
    s_val = f"{ans:.12e}"
    # Replace e+06 with e+06 so it looks natural, python does e+06, C++ does e+06 or e+006
    return s_val

def solve():
    return f_of_u64(1234567890123456789)

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

Java

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;

public class Euler776 {

    static class Stats {
        BigInteger count = BigInteger.ZERO;
        BigInteger sum = BigInteger.ZERO;
    }

    public static String fOfU64(long n) {
        String s = Long.toString(n);
        int L = s.length();
        int maxSum = 9 * L;

        Stats[] tight = new Stats[maxSum + 1];
        Stats[] loose = new Stats[maxSum + 1];
        for (int i = 0; i <= maxSum; i++) {
            tight[i] = new Stats();
            loose[i] = new Stats();
        }

        tight[0].count = BigInteger.ONE;
        BigInteger ten = BigInteger.TEN;

        for (int pos = 0; pos < L; ++pos) {
            int limit = s.charAt(pos) - '0';
            Stats[] ntight = new Stats[maxSum + 1];
            Stats[] nloose = new Stats[maxSum + 1];
            for (int i = 0; i <= maxSum; i++) {
                ntight[i] = new Stats();
                nloose[i] = new Stats();
            }

            for (int sum = 0; sum <= maxSum; ++sum) {
                if (tight[sum].count.compareTo(BigInteger.ZERO) != 0) {
                    for (int d = 0; d <= limit; ++d) {
                        int ns = sum + d;
                        Stats dst = (d == limit) ? ntight[ns] : nloose[ns];
                        dst.count = dst.count.add(tight[sum].count);
                        BigInteger addSum = tight[sum].sum.multiply(ten)
                                .add(tight[sum].count.multiply(BigInteger.valueOf(d)));
                        dst.sum = dst.sum.add(addSum);
                    }
                }

                if (loose[sum].count.compareTo(BigInteger.ZERO) != 0) {
                    for (int d = 0; d <= 9; ++d) {
                        int ns = sum + d;
                        Stats dst = nloose[ns];
                        dst.count = dst.count.add(loose[sum].count);
                        BigInteger addSum = loose[sum].sum.multiply(ten)
                                .add(loose[sum].count.multiply(BigInteger.valueOf(d)));
                        dst.sum = dst.sum.add(addSum);
                    }
                }
            }

            tight = ntight;
            loose = nloose;
        }

        MathContext mc = new MathContext(100);
        BigDecimal ans = BigDecimal.ZERO;

        for (int sum = 1; sum <= maxSum; ++sum) {
            BigInteger totalSum = tight[sum].sum.add(loose[sum].sum);
            if (totalSum.equals(BigInteger.ZERO))
                continue;

            BigDecimal term = new BigDecimal(totalSum).divide(new BigDecimal(sum), mc);
            ans = ans.add(term, mc);
        }

        // Format to scientific notation with 12 decimal places
        // The format %e inherently uses 6 decimal places by default, so %.12e is used
        return String.format(java.util.Locale.US, "%.12e", ans);
    }

    public static String solve() {
        return fOfU64(1234567890123456789L);
    }

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