Problem 840: Sum of Products
View on Project EulerProject Euler Problem 840 Solution
EulerSolve provides an optimized solution for Project Euler Problem 840, Sum of Products, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary For each positive integer \(n\), the problem assigns a weight \(A(n)\). The convention is \(A(1)=1\); for every prime \(p\), \(A(p)=1\); and for composite products the rule is Leibniz-like: $A(ab)=aA(b)+bA(a).$ If \(\lambda=(\lambda_1,\dots,\lambda_k)\) is a partition of \(s\), its weight is the product of the part-weights: $w(\lambda)=\prod_{i=1}^{k} A(\lambda_i).$ Let \(P(s)\) be the sum of these weights over all integer partitions of \(s\). The required result is the cumulative total $S(N)=\sum_{s=1}^{N} P(s)\pmod{999676999}.$ The published implementations evaluate this for \(N=50000\), so the main challenge is to compute every \(P(s)\) up to that bound without enumerating partitions explicitly. Mathematical Approach Step 1: Identify the Part-Weight Function The recurrence for \(A(n)\) is exactly the arithmetic-derivative rule on all \(n\ge 2\), with the extra convention \(A(1)=1\) so that parts equal to \(1\) contribute a neutral factor instead of killing the product....
Detailed mathematical approach
Problem Summary
For each positive integer \(n\), the problem assigns a weight \(A(n)\). The convention is \(A(1)=1\); for every prime \(p\), \(A(p)=1\); and for composite products the rule is Leibniz-like:
$A(ab)=aA(b)+bA(a).$
If \(\lambda=(\lambda_1,\dots,\lambda_k)\) is a partition of \(s\), its weight is the product of the part-weights:
$w(\lambda)=\prod_{i=1}^{k} A(\lambda_i).$
Let \(P(s)\) be the sum of these weights over all integer partitions of \(s\). The required result is the cumulative total
$S(N)=\sum_{s=1}^{N} P(s)\pmod{999676999}.$
The published implementations evaluate this for \(N=50000\), so the main challenge is to compute every \(P(s)\) up to that bound without enumerating partitions explicitly.
Mathematical Approach
Step 1: Identify the Part-Weight Function
The recurrence for \(A(n)\) is exactly the arithmetic-derivative rule on all \(n\ge 2\), with the extra convention \(A(1)=1\) so that parts equal to \(1\) contribute a neutral factor instead of killing the product.
Writing
$n=\prod_{j=1}^{r} p_j^{\alpha_j},$
the product rule gives the closed form
$A(n)=\sum_{j=1}^{r}\alpha_j p_j^{\alpha_j-1}\prod_{\ell\ne j} p_\ell^{\alpha_\ell}=n\sum_{j=1}^{r}\frac{\alpha_j}{p_j}\qquad (n\ge 2).$
Examples are immediate:
$A(2)=1,\qquad A(4)=4,\qquad A(6)=6\left(\frac{1}{2}+\frac{1}{3}\right)=5,\qquad A(12)=12\left(\frac{2}{2}+\frac{1}{3}\right)=16.$
This formula explains why the implementations can fill the entire weight table once the smallest prime factor of each number is known.
Step 2: Convert Weighted Partitions into a Generating Function
For each \(s\ge 0\), define
$P(s)=\sum_{\lambda\vdash s} w(\lambda),\qquad P(0)=1,$
where \(\lambda\vdash s\) means that \(\lambda\) is an integer partition of \(s\).
If a part size \(m\) appears \(c\) times, it contributes the factor \(A(m)^c x^{cm}\). Summing over all multiplicities \(c\ge 0\) gives a geometric series, so the ordinary generating function is
$\sum_{s\ge 0} P(s)x^s=\prod_{m\ge 1}\left(1+A(m)x^m+A(m)^2x^{2m}+\cdots\right)=\prod_{m\ge 1}\frac{1}{1-A(m)x^m}.$
This is the central structural observation: the problem is a weighted partition problem, not a search over factorizations or compositions.
Step 3: Extract the Dynamic Programming Recurrence
Let \(P_m(s)\) denote the weighted partition total for \(s\) using only part sizes at most \(m\). Then
$P_0(0)=1,\qquad P_0(s)=0\ \text{for}\ s>0,$
and for each \(m\ge 1\) we split partitions into those that use no part \(m\) and those that use at least one part \(m\):
$P_m(s)=P_{m-1}(s)+A(m)P_m(s-m)\qquad (s\ge m).$
The term \(P_{m-1}(s)\) covers partitions that skip \(m\), while \(A(m)P_m(s-m)\) removes one copy of \(m\) from a partition that uses it. This is exactly the recurrence of an unbounded knapsack or complete-partition DP.
Step 4: Recover the Requested Prefix Sum
After processing all part sizes up to \(N\), the coefficient table contains \(P(0),P(1),\dots,P(N)\). The requested answer is then only a prefix sum:
$S(N)=\sum_{s=1}^{N} P(s)\pmod{M},\qquad M=999676999.$
No separate combinatorial argument is needed at the end. Once the coefficients are known, the final accumulation is straightforward.
Worked Example: \(s=4\)
The needed part-weights are
$A(1)=1,\qquad A(2)=1,\qquad A(3)=1,\qquad A(4)=4.$
The partitions of \(4\) and their weights are
$\begin{aligned} (4) &\longrightarrow 4,\\ (3+1) &\longrightarrow 1\cdot 1=1,\\ (2+2) &\longrightarrow 1\cdot 1=1,\\ (2+1+1) &\longrightarrow 1,\\ (1+1+1+1) &\longrightarrow 1. \end{aligned}$
Therefore
$P(4)=4+1+1+1+1=8.$
Continuing the same process gives a useful checkpoint:
$P(10)=164,\qquad \sum_{s=1}^{10} P(s)=396.$
How the Code Works
The C++, Python, and Java implementations first build a smallest-prime-factor table up to \(N\). That table makes the weight computation linear after the sieve: if \(x\) is prime then its weight is \(1\), and if \(x=pq\) with \(p\) the smallest prime factor, then the product rule gives \(A(x)=pA(q)+q\), where \(q<x\) has already been processed.
Next the implementation runs a one-dimensional complete-partition DP. Part sizes are processed in increasing order, and target sums are scanned upward from the current part size to \(N\). That update order is what allows unlimited reuse of the same part size while still counting each partition only once, independent of the ordering of its parts.
After the coefficient table has been filled, the implementation accumulates the prefix total from \(1\) through \(N\), reducing after every arithmetic step modulo \(999676999\). The Python version follows the same numerical method by delegating to the compiled solver, so all three language versions share the same mathematics.
Complexity Analysis
Let \(N\) be the target bound. Building the smallest-prime-factor sieve costs \(O(N\log\log N)\) time and \(O(N)\) memory. Filling the weight sequence from that sieve is \(O(N)\). The dominant phase is the weighted partition DP, whose nested loops over part size and target sum require \(O(N^2)\) modular operations. The full method therefore runs in \(O(N^2)\) time and uses \(O(N)\) memory.
Footnotes and References
- Problem page: https://projecteuler.net/problem=840
- Arithmetic derivative: Wikipedia - Arithmetic derivative
- Integer partitions: Wikipedia - Partition (number theory)
- Generating functions: Wikipedia - Generating function
- Unbounded knapsack dynamic programming: cp-algorithms - Knapsack Problem