Open Source · 7 Languages · 900+ Problems

Project Euler Solutions

Complete solutions in C++, Python & Java — with step-by-step mathematical explanations

All Problems

Problem 537: Counting Tuples

View on Project Euler

Project Euler Problem 537 Solution

EulerSolve provides an optimized solution for Project Euler Problem 537, Counting Tuples, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary Let \(\pi(x)\) denote the number of primes not exceeding \(x\), and let \(p_r\) be the \(r\)-th prime. The problem asks for the number \(T(n,k)\) of ordered \(k\)-tuples of positive integers \((x_1,\dots,x_k)\) such that $\pi(x_1)+\pi(x_2)+\cdots+\pi(x_k)=n,$ with the final result taken modulo $M=1004535809.$ The decisive observation is that many different integers share the same prime-count value, and those multiplicities are controlled exactly by consecutive prime gaps. Mathematical Approach The implementations turn the tuple-counting problem into coefficient extraction from a generating function, and then accelerate the polynomial arithmetic with an NTT. Step 1: Group Integers by Their Prime-Count Value For \(r=0\), there is only one positive integer with \(\pi(x)=0\), namely \(x=1\). Therefore $a_0=1.$ For every \(r\ge 1\), the condition \(\pi(x)=r\) means that \(x\) lies between the \(r\)-th and \((r+1)\)-st primes: $\pi(x)=r \iff p_r \le x \le p_{r+1}-1.$ So the number of such integers is exactly the prime gap $a_r=p_{r+1}-p_r \qquad (r\ge 1).$ These coefficients are the basic weights of the whole problem: \(a_r\) counts how many choices one coordinate has if it contributes exactly \(r\) to the total sum of prime counts....

Detailed mathematical approach

Problem Summary

Let \(\pi(x)\) denote the number of primes not exceeding \(x\), and let \(p_r\) be the \(r\)-th prime. The problem asks for the number \(T(n,k)\) of ordered \(k\)-tuples of positive integers \((x_1,\dots,x_k)\) such that

$\pi(x_1)+\pi(x_2)+\cdots+\pi(x_k)=n,$

with the final result taken modulo

$M=1004535809.$

The decisive observation is that many different integers share the same prime-count value, and those multiplicities are controlled exactly by consecutive prime gaps.

Mathematical Approach

The implementations turn the tuple-counting problem into coefficient extraction from a generating function, and then accelerate the polynomial arithmetic with an NTT.

Step 1: Group Integers by Their Prime-Count Value

For \(r=0\), there is only one positive integer with \(\pi(x)=0\), namely \(x=1\). Therefore

$a_0=1.$

For every \(r\ge 1\), the condition \(\pi(x)=r\) means that \(x\) lies between the \(r\)-th and \((r+1)\)-st primes:

$\pi(x)=r \iff p_r \le x \le p_{r+1}-1.$

So the number of such integers is exactly the prime gap

$a_r=p_{r+1}-p_r \qquad (r\ge 1).$

These coefficients are the basic weights of the whole problem: \(a_r\) counts how many choices one coordinate has if it contributes exactly \(r\) to the total sum of prime counts.

Step 2: Write the Tuple Recurrence

If the first coordinate contributes \(i\), there are \(a_i\) possible values for it, and the remaining \(k-1\) coordinates must contribute \(n-i\). This gives the convolution recurrence

$T(n,k)=\sum_{i=0}^{n} a_i\,T(n-i,k-1).$

The natural initial conditions are

$T(0,0)=1,\qquad T(n,0)=0 \text{ for } n>0,$

and for one coordinate we simply have

$T(n,1)=a_n.$

So the combinatorics is already telling us that each extra coordinate adds one more discrete convolution with the sequence \((a_0,a_1,a_2,\dots)\).

Step 3: Convert the Recurrence into a Generating Function

Introduce the ordinary generating series

$A(x)=\sum_{r\ge 0} a_r x^r$

and, for fixed \(k\),

$F_k(x)=\sum_{n\ge 0} T(n,k)x^n.$

The recurrence from the previous step becomes

$F_k(x)=A(x)\,F_{k-1}(x).$

Iterating this identity and using \(F_0(x)=1\), we get

$F_k(x)=A(x)^k.$

Therefore the answer is the coefficient of \(x^n\):

$\boxed{T(n,k)=[x^n]A(x)^k.}$

This is the exact mathematical statement implemented by the C++, Python, and Java versions.

Step 4: Truncation Is Safe

When we only need \([x^n]A(x)^k\), no coefficient of degree greater than \(n\) can ever become relevant later. Every multiplication adds nonnegative exponents, so once a term has degree \(>n\), future multiplications can only push it to even higher degree.

Hence after every polynomial product we may safely discard all terms above degree \(n\) and keep only

$c_0+c_1x+\cdots+c_nx^n.$

This is why the implementations always work with truncated polynomials of length \(n+1\), even while repeatedly squaring during exponentiation.

Step 5: Use an NTT-Friendly Modulus

A naive convolution of two degree-\(n\) polynomials costs \(O(n^2)\), which is too slow for the target size. The chosen modulus is

$M=1004535809=479\cdot 2^{21}+1.$

Because \(M-1\) contains a large power of \(2\), every transform length

$L=2^m \le 2^{21}$

has the necessary primitive \(L\)-th roots of unity modulo \(M\). In particular, the primitive root \(3\) generates all the roots needed by the transform. So polynomial multiplication can be done exactly modulo \(M\) with a Number Theoretic Transform in

$O(L\log L)$

time instead of quadratic time.

Worked Example: \(T(3,2)\)

The first few coefficient buckets are easy to read off from the primes:

$a_0=1,\qquad a_1=3-2=1,\qquad a_2=5-3=2,\qquad a_3=7-5=2.$

Indeed, the corresponding sets are

$\pi(x)=0 \Rightarrow \{1\},\qquad \pi(x)=1 \Rightarrow \{2\},\qquad \pi(x)=2 \Rightarrow \{3,4\},\qquad \pi(x)=3 \Rightarrow \{5,6\}.$

Now

$T(3,2)=[x^3]A(x)^2=a_0a_3+a_1a_2+a_2a_1+a_3a_0=2+2+2+2=8.$

The eight ordered pairs are

$(1,5),(1,6),(2,3),(2,4),(3,2),(4,2),(5,1),(6,1).$

This small example shows exactly why coefficient extraction matches the tuple count.

How the Code Works

The implementations first generate enough consecutive primes to know the gaps \(p_{r+1}-p_r\) for all \(0\le r\le n\). From those gaps they build the truncated coefficient list of \(A(x)\) up to degree \(n\).

Polynomial multiplication is then performed by padding both coefficient arrays to the next power of two, applying the forward NTT, multiplying pointwise, and applying the inverse NTT. After every product, the result is truncated back to degree \(n\).

To obtain \(A(x)^k\), the implementations use binary exponentiation in the polynomial ring: when the current bit of \(k\) is set, the running result is multiplied by the current base; after that, the base is squared. Because every intermediate product is truncated, the degree never grows beyond what is needed.

Once the exponentiation finishes, the coefficient of \(x^n\) is returned modulo \(M\).

Complexity Analysis

Let \(B\) be a sieve bound large enough to contain \(p_{n+1}\), and let \(L\) be the smallest power of two satisfying \(L\ge 2n+1\). Generating the needed primes by a sieve costs \(O(B\log\log B)\) time and \(O(B)\) memory.

Each truncated polynomial multiplication costs \(O(L\log L)\), and binary exponentiation performs \(O(\log k)\) such multiplications. Therefore the full running time is

$O(B\log\log B + L\log L\log k).$

Since \(L=O(n)\), the polynomial part is usually summarized as

$O(n\log n\log k).$

The memory usage is \(O(B+L)\) while the sieve and polynomial buffers coexist.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=537
  2. Prime-counting function: Wikipedia — Prime-counting function
  3. Prime gap: Wikipedia — Prime gap
  4. Generating function: Wikipedia — Generating function
  5. Number Theoretic Transform: cp-algorithms — Operations on polynomials and series

Mathematical approach · C++ solution · Python solution · Java solution

Previous: Problem 536 · All Project Euler solutions · Next: Problem 538

Need help with a problem? Ask me! 💡
e
✦ Euler GLM 5.2
Hi! I'm Euler. Ask me anything about Project Euler problems, math concepts, or solution approaches! 🧮