Problem 1008: Functional Inverse
View on Project EulerProject Euler Problem 1008 Solution
EulerSolve provides an optimized solution for Project Euler Problem 1008, Functional Inverse, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary For a prime modulus \(M\), an \((N,M)\)-functional inverse of \(x^2\) is a lowest-degree monic polynomial \(Q(x)\) whose coefficients are represented in \(\{0,\ldots,M-1\}\) and which satisfies \[Q(n^2)\equiv n\pmod M\qquad(0\le n\le N).\] Here \(N=10^7\), \(M=p=10^9+7\), and only the coefficient of \(x^{10}\) is required. Constructing the degree-\(N\) interpolation polynomial, or even storing all its coefficients, is unnecessary: the calculation can be truncated rigorously at degree \(9\). Conventions and the meaning of the inverse The notation \([x^r]F(x)\) means the coefficient of \(x^r\) in \(F\). All equalities below are in \(\mathbb F_p\), unless explicitly presented as rational identities. A fraction \(a/b\) means \(a\,b^{-1}\pmod p\), not integer division or floating-point division. Since \(p\) is prime, every nonzero denominator has a unique inverse. Choosing its representative between \(0\) and \(p-1\) gives the coefficient format required by the problem. The task does not ask for an inverse of squaring on the whole field: \(n\) and \(-n\) have the same square. It prescribes one value only at each of the selected square nodes. The bound \(2N<p\) ensures that two selected positive indices never supply opposite square roots of the same node....
Detailed mathematical approach
Problem Summary
For a prime modulus \(M\), an \((N,M)\)-functional inverse of \(x^2\) is a lowest-degree monic polynomial \(Q(x)\) whose coefficients are represented in \(\{0,\ldots,M-1\}\) and which satisfies
\[Q(n^2)\equiv n\pmod M\qquad(0\le n\le N).\]
Here \(N=10^7\), \(M=p=10^9+7\), and only the coefficient of \(x^{10}\) is required. Constructing the degree-\(N\) interpolation polynomial, or even storing all its coefficients, is unnecessary: the calculation can be truncated rigorously at degree \(9\).
Conventions and the meaning of the inverse
The notation \([x^r]F(x)\) means the coefficient of \(x^r\) in \(F\). All equalities below are in \(\mathbb F_p\), unless explicitly presented as rational identities. A fraction \(a/b\) means \(a\,b^{-1}\pmod p\), not integer division or floating-point division. Since \(p\) is prime, every nonzero denominator has a unique inverse. Choosing its representative between \(0\) and \(p-1\) gives the coefficient format required by the problem.
The task does not ask for an inverse of squaring on the whole field: \(n\) and \(-n\) have the same square. It prescribes one value only at each of the selected square nodes. The bound \(2N<p\) ensures that two selected positive indices never supply opposite square roots of the same node. This is why interpolation, rather than a power-series expansion of the real square root, is the appropriate starting point.
Mathematical Approach
Interpolation at the square nodes
Work in the field \(\mathbb F_p\). The nodes \(0^2,1^2,\ldots,N^2\) are distinct: if \(i^2\equiv j^2\pmod p\), then \(p\mid(i-j)(i+j)\), but \(0\le i,j\le N\) and \(i+j\le2N<p\). Hence there is a unique polynomial \(P_N\) of degree at most \(N\) such that \(P_N(n^2)=n\) for every required node.
Write that polynomial in Newton form:
\[P_N(x)=\sum_{k=1}^{N}d_k\prod_{j=0}^{k-1}(x-j^2).\]
The term \(k=0\) vanishes because the prescribed value at \(0\) is \(0\).
Here is an explicit existence and uniqueness argument. For a fixed \(k\), define the Lagrange basis below. At \(x=j^2\), its \(i\)-th member is \(1\) when \(i=j\) and \(0\) otherwise. Thus their weighted sum has the prescribed values. If two degree-at-most-\(k\) polynomials had those values, their difference would have \(k+1\) distinct roots; a nonzero polynomial of degree at most \(k\) cannot do so.
\[L_{i,k}(x)=\prod_{\substack{0\le j\le k\\j\ne i}}\frac{x-j^2}{i^2-j^2},\qquad P_k(x)=\sum_{i=0}^{k}iL_{i,k}(x).\]
Newton's form adds the nodes one at a time. The difference \(P_k-P_{k-1}\) vanishes at the first \(k\) nodes, so it is a constant multiple of their monic product \(B_k\). Evaluating at the new node determines that constant. Since \(P_{k-1}\) has degree at most \(k-1\), \(d_k\) is also the coefficient of \(x^k\) in the Lagrange expression for \(P_k\). Reading off that leading coefficient gives the divided-difference sum used next.
\[\begin{aligned}B_k(x)&=\prod_{j=0}^{k-1}(x-j^2),\\P_k(x)&=P_{k-1}(x)+d_kB_k(x),\\d_k&=\frac{k-P_{k-1}(k^2)}{B_k(k^2)}=[x^k]P_k(x).\end{aligned}\]
Closed form for the Newton coefficients
The divided difference on \(0^2,\ldots,k^2\) is
\[d_k=\sum_{i=1}^{k}\frac{i} {\displaystyle\prod_{\substack{0\le j\le k\\j\ne i}}(i^2-j^2)}.\]
To evaluate the denominator, keep the two linear products separate. In the first product there are exactly \(k-i\) negative factors. In the second, the consecutive product from \(i\) to \(i+k\) must have its missing factor \(2i\) removed. For \(1\le i\le k\), this gives
\[\begin{aligned}\prod_{\substack{0\le j\le k\\j\ne i}}(i-j)&=i!\,(-1)^{k-i}(k-i)!,\\\prod_{\substack{0\le j\le k\\j\ne i}}(i+j)&=\frac{(i+k)!}{(i-1)!\,2i}.\end{aligned}\]
Multiplying these expressions cancels \(i!/(i-1)!=i\) against \(2i\). All factorials involved are invertible modulo \(p\), since their arguments are at most \(2k\le2N<p\). Thus
\[\prod_{\substack{0\le j\le k\\j\ne i}}(i^2-j^2)=\frac{(-1)^{k-i}}{2}(k-i)!(k+i)!.\]
\[d_k=\sum_{i=1}^{k}\frac{2i(-1)^{k-i}}{(k-i)!(k+i)!}.\]
The remaining sum can be evaluated without leaving an unexplained cancellation. Multiply by \((2k)!\), put \(r=k-i\), and name the result \(S_k\). The factorial denominator becomes a binomial coefficient:
\[S_k=(2k)!d_k=\sum_{r=0}^{k-1}(-1)^r\,2(k-r)\binom{2k}{r}.\]
\[2(k-r)\binom{2k}{r}=2k\left(\binom{2k-1}{r}-\binom{2k-1}{r-1}\right).\]
To handle the alternating sums, use Pascal's identity in each summand. After shifting the second sum by one index, every interior term cancels and only the last term remains. We use the convention that a binomial coefficient with lower index \(-1\) is zero and that an empty sum is zero.
\[\begin{aligned}\sum_{r=0}^{m}(-1)^r\binom{n}{r}&=\sum_{r=0}^{m}(-1)^r\binom{n-1}{r}-\sum_{r=0}^{m-1}(-1)^r\binom{n-1}{r}\\&=(-1)^m\binom{n-1}{m}.\end{aligned}\]
Substitute the binomial difference into \(S_k\). The minus sign in that difference becomes a plus after the index shift, because the sign also changes. Apply the partial-sum identity twice, and then use the ratio of the two adjacent binomial coefficients. The calculation is valid for \(k=1\) as well under the empty-sum convention:
\[\begin{aligned}\frac{S_k}{2k}&=\sum_{r=0}^{k-1}(-1)^r\binom{2k-1}{r}+\sum_{r=0}^{k-2}(-1)^r\binom{2k-1}{r}\\&=(-1)^{k-1}\left(\binom{2k-2}{k-1}-\binom{2k-2}{k-2}\right)\\&=\frac{(-1)^{k-1}}{k}\binom{2k-2}{k-1}.\end{aligned}\]
\[d_k=\frac{2(-1)^{k-1}}{(2k)!}\binom{2k-2}{k-1}=\boxed{\frac{(-1)^{k-1}}{(2k-1)k!(k-1)!}}.\]
Normalize the products and retain only degree \(9\)
Define
\[A_k(x)=\prod_{j=1}^{k}\left(1-\frac{x}{j^2}\right), \qquad A_0(x)=1.\]
Since
\[\prod_{j=0}^{k-1}(x-j^2) =x(-1)^{k-1}((k-1)!)^2A_{k-1}(x),\]
the signs and factorials cancel against \(d_k\), leaving the particularly simple identity
\[\boxed{\frac{P_N(x)}x= \sum_{k=1}^{N}\frac{A_{k-1}(x)}{k(2k-1)}}.\]
Consequently \([x^{10}]P_N=[x^9](P_N/x)\). Put \(a_d^{(k)}=[x^d]A_k\) and let \(s_d^{(k)}\) denote the accumulated coefficient after terms \(1,\ldots,k\). For \(1\le d\le9\), each step is
\[s_d^{(k)}=s_d^{(k-1)} +\frac{a_d^{(k-1)}}{k(2k-1)},\]
\[a_d^{(k)}=a_d^{(k-1)} -\frac{a_{d-1}^{(k-1)}}{k^2}.\]
The \(a_d\) array is updated from degree \(9\) down to \(1\), so every right-hand side still belongs to \(A_{k-1}\). Dropping terms above degree \(9\) is exact, not an approximation: later multiplications are by polynomials with nonnegative degree and can never move a discarded coefficient down to degree \(9\).
More explicitly, the scalar multiplying \(A_{k-1}\) is \(d_k(-1)^{k-1}((k-1)!)^2=1/(k(2k-1))\). Initialize the coefficient state and the sum as follows; the constant coefficient of \(A_k\) remains \(1\) throughout, so it never needs updating.
\[a_0^{(0)}=1,\qquad a_d^{(0)}=0\ (1\le d\le9),\qquad s_d^{(0)}=0\ (0\le d\le9).\]
There are two separate ordering requirements. First accumulate the contribution from \(A_{k-1}\), then multiply by the new factor to obtain \(A_k\). Inside that multiplication, update degrees downwards. An upward update would use the already changed \(a_{d-1}\). Starting from \(1\), for example, it would incorrectly create an \(x^2/k^4\) term after multiplying by just \(1-x/k^2\), even though that product has degree one.
The formal reason truncation is safe is that we compute in the quotient ring \(\mathbb F_p[x]/(x^{10})\). Reduction modulo \(x^{10}\) commutes with addition and multiplication. Equivalently, the coefficient of degree \(d\le9\) in a product uses only input coefficients of degrees at most \(d\). No discarded high-degree term can affect any stored coefficient:
\[[x^d](F(x)G(x))=\sum_{r=0}^{d}([x^r]F(x))([x^{d-r}]G(x)),\qquad 0\le d\le9.\]
A serial run therefore stores ten product coefficients but only the scalar \(s_9\). The other sum coefficients never feed into \(s_9\). All ten product coefficients are still essential: repeated updates can move information from degree zero through the intermediate degrees to degree nine. Also, \(a_9^{(k-1)}=0\) for \(k<10\), which explains why the first nine summands contribute nothing to the interpolation part of the requested coefficient.
A small worked interpolation example
Take \(N=3\) and keep rational fractions temporarily to make the algebra readable. These identities can then be reduced modulo any prime \(p>6\). The first normalized products are
\[A_0=1,\qquad A_1=1-x,\qquad A_2=(1-x)(1-x/4)=1-\frac54x+\frac14x^2.\]
The weights for \(k=1,2,3\) are \(1,1/6,1/15\). Each weight uses the product before its own factor has been inserted. Substituting and collecting coefficients yields
\[\begin{aligned}\frac{P_3(x)}x&=1+\frac{1-x}{6}+\frac{1-5x/4+x^2/4}{15}\\&=\frac{37}{30}-\frac{x}{4}+\frac{x^2}{60},\\P_3(x)&=\frac{37}{30}x-\frac14x^2+\frac1{60}x^3.\end{aligned}\]
\[P_3(0)=0,\qquad P_3(1)=1,\qquad P_3(4)=2,\qquad P_3(9)=3.\]
This checks both the prescribed values and the coefficient formula \(d_3=1/60\). It also exposes the distinction between interpolation and the monic condition: for the target modulus, this small interpolant has leading coefficient \(1/60\ne1\), so interpolation alone is not the complete construction. The monic correction is derived below.
Batch inversion removes millions of exponentiations
Let \(u_k=k(2k-1)\). All \(u_k\) are nonzero in \(\mathbb F_p\), because \(1\le k\le N<p\) and \(2k-1<p\). In a batch \(u_1,\ldots,u_m\), form prefix products \(U_0=1\), \(U_i=U_{i-1}u_i\). Compute only \(U_m^{-1}=U_m^{p-2}\) and walk backwards:
\[u_i^{-1}=U_{i-1}U_i^{-1}, \qquad U_{i-1}^{-1}=U_i^{-1}u_i.\]
Thus one Fermat exponentiation supplies every reciprocal in a batch. Moreover \(k^{-1}=(2k-1)u_k^{-1}\), so squaring it gives \(k^{-2}\) for the product update. The implementations use batches of \(32768\) entries.
The subscripts in the batch formula are local to that batch; its first global index need not be \(1\). During the backward pass, maintain the invariant that the running inverse equals \(U_i^{-1}\). Multiplying by the still-unmodified prefix \(U_{i-1}\) yields \(u_i^{-1}\), and multiplying the running inverse by \(u_i\) prepares the next iteration. The prefix slot can then be overwritten by its recovered inverse, because subsequent iterations only need earlier slots.
As a concrete check of this general trick, invert \(2,3,5\) modulo \(11\). The prefix products are \(1,2,6,8\), and \(8^{-1}=7\). The backward pass first gives \(5^{-1}=6\cdot7=9\), then \(3^{-1}=2\cdot2=4\), then \(2^{-1}=1\cdot6=6\), all modulo \(11\). Only one exponentiation was required. In the actual algorithm the inputs are the nonzero values \(k(2k-1)\), and every multiplication is reduced modulo \(p\).
Making the interpolant monic
Because each Newton basis polynomial is monic, the leading coefficient of \(P_N\) is
\[c_N=d_N= \frac{(-1)^{N-1}N}{(2N-1)(N!)^2}.\]
The polynomial that vanishes at every interpolation node is
\[V_N(x)=\prod_{j=0}^{N}(x-j^2) =(-1)^N(N!)^2xA_N(x).\]
The formula for \(d_N\) is nonzero: its numerator is a sign and none of its denominator factors vanishes. Therefore \(P_N\) has degree exactly \(N\), not merely at most \(N\). To describe every possible solution, let \(F\) be any polynomial with the prescribed values. Each linear factor \(x-j^2\) divides \(F-P_N\); because the roots are distinct, their product divides it. Hence
\[F(x)=P_N(x)+V_N(x)R(x),\qquad R(x)\in\mathbb F_p[x].\]
If \(R\ne0\), then \(\deg(V_NR)=N+1+\deg R>\deg P_N\), so the highest term cannot cancel. Thus the only solution of degree at most \(N\) is \(P_N\). If it is not monic, a monic solution of the next degree requires \(R\) to be a constant, and its leading coefficient forces that constant to be exactly \(1\). This proves both minimality and uniqueness. Simply dividing \(P_N\) by \(c_N\) would instead change the required value at \(1\) from \(1\) to \(c_N^{-1}\), so scaling is not an admissible repair.
If \(c_N=1\), the interpolant itself is already the unique lowest-degree monic solution. Otherwise no polynomial of degree at most \(N\) can be another solution, and the lowest possible monic degree is \(N+1\); adding the monic polynomial \(V_N\) gives
\[Q(x)=P_N(x)+V_N(x).\]
For the target instance the code verifies \(c_N\ne1\). The desired coefficient is therefore obtained without constructing either large polynomial:
\[\boxed{[x^{10}]Q=s_9^{(N)} +(-1)^N(N!)^2a_9^{(N)}}.\]
Note on the published small example. Read literally, the “lowest-degree” wording has a special edge case at \((N,M)=(2,7)\): there \(c_2=1\), so \(P_2(x)=x^2\) is already monic and satisfies the three values. The displayed cubic is \(P_2+V_2\), which satisfies them as well. This editorial edge case does not affect the requested instance, for which \(c_N\ne1\).
For a separate example with no monic edge case, take \((N,p)=(2,11)\). The rational interpolant \((7x-x^2)/6\) reduces to \(3x+9x^2\). Adding the vanishing polynomial gives the following cubic; its values at \(0,1,4\) are respectively \(0,1,2\) modulo \(11\), and its leading coefficient is \(1\). This illustrates why the correction changes coefficients while preserving every required value.
\[\begin{aligned}V_2(x)&=x(x-1)(x-4)\equiv x^3+6x^2+4x\pmod{11},\\Q(x)&=P_2(x)+V_2(x)\equiv x^3+4x^2+7x\pmod{11}.\end{aligned}\]
Combining independently processed blocks
The C++ program can process consecutive index intervals in parallel. For a block \(B\), write \(g_k(x)=1-x/k^2\) and store, through degree \(9\),
\[G_B=\prod_{k\in B}g_k,\qquad H_B=\sum_{k\in B}\frac1{k(2k-1)} \prod_{\substack{j\in B\\j<k}}g_j.\]
If block \(L\) immediately precedes block \(R\), their summaries combine as
\[\boxed{G_{LR}=G_LG_R,\qquad H_{LR}=H_L+G_LH_R}.\]
This operation is associative, but order matters; the implementation merges blocks in increasing-index order. Their factorial products are multiplied at the same time. Truncated polynomial multiplication is sufficient because coefficients above degree \(9\) cannot contribute below that degree.
The block rule can also be read as an update of the incoming global state: \((S,A)\mapsto(S+AH_B,AG_B)\). Contributions from the right block need every factor in the left block before them, which accounts for \(G_LH_R\). For three consecutive blocks, either parenthesization produces exactly the same expression below, proving associativity. The empty block has summary \((G,H)=(1,0)\).
\[G_{LMR}=G_LG_MG_R,\qquad H_{LMR}=H_L+G_LH_M+G_LG_MH_R.\]
This also explains why a worker must retain all ten coefficients of \(H_B\), even though the final answer needs only degree nine. An earlier product mixes the degrees according to the convolution below. The merging code may keep only the ninth coefficient of the global sum, but not only the ninth coefficient of each worker's sum. Inversion batches and parallel blocks serve different purposes: a worker can process many inversion batches within its own consecutive interval.
\[[x^9](AH_B)=\sum_{r=0}^{9}([x^r]A)([x^{9-r}]H_B).\]
Correctness Argument
Lemma 1. The square nodes are distinct in \(\mathbb F_p\), so \(P_N\) exists and is the only interpolant of degree at most \(N\).
Lemma 2. Evaluating the divided-difference denominator and the alternating binomial sum gives the stated \(d_k\). Substitution into Newton's formula gives \(P_N/x=\sum A_{k-1}/(k(2k-1))\).
Lemma 3. The descending recurrence maintains \(a_d^{(k)}=[x^d]A_k\), while the accumulation maintains \(s_d^{(k)}=[x^d]\sum_{i=1}^{k}A_{i-1}/(i(2i-1))\). Degree-\(9\) truncation preserves these invariants exactly.
Lemma 4. Batch inversion returns the true inverse of each nonzero \(u_k\): \(U_{i-1}U_i^{-1}=1/u_i\). Hence it changes only how the same recurrence is evaluated.
Lemma 5. Every polynomial satisfying the data differs from \(P_N\) by a multiple of \(V_N\). The leading-coefficient test therefore selects \(P_N\) exactly when it is already monic and otherwise selects the monic polynomial \(P_N+V_N\) of the smallest possible degree.
Lemma 6. The ordered block identity inserts \(G_L\) before every contribution originating in \(R\); induction over merged blocks therefore reproduces the serial sum and product.
Combining the lemmas, the final expression is exactly the coefficient of \(x^{10}\) in the required lowest-degree monic functional inverse.
How the Code Works
In C++, Block.product[d] represents the truncated product coefficient and Block.sum[d] represents the corresponding weighted sum. The serial specialization of calculate_block keeps only sum[9]; parallel workers retain the full sum. Python and Java store the same product in coefficients and the ninth sum coefficient in interpolation_coefficient and interpolationCoefficient, respectively. The mathematical state is the same in all three implementations.
At the start of serial iteration \(k\), the array contains \(A_{k-1}\), the accumulator contains \(s_9^{(k-1)}\), and the factorial variable contains \((k-1)!\). Retrieve the batch weight, add its contribution to the accumulator, update the array downwards, and multiply the factorial variable by \(k\). These actions re-establish the same invariant for iteration \(k+1\). After the final iteration the state is exactly \((A_N,s_9^{(N)},N!)\), which is all the final formula needs.
The C++ default uses at most \(16\) worker threads, while its optional --threads argument can select another supported count. Workers process consecutive intervals and merge in their original order. Python and Java evaluate the serial recurrence. Binary modular exponentiation is provided by C++ power, Python's three-argument pow, and Java modPow. For this modulus, multiplying two reduced residues gives less than \((10^9+7)^2<2^{63}\), so C++ 64-bit arithmetic and Java long suffice; a 32-bit product would not suffice.
For validation, each implementation also includes a direct Lagrange interpolator that constructs the whole polynomial for \(1\le N\le40\). It uses a different construction from the optimized Newton recurrence. Comparisons with that reference include the already-monic case at \(N=1\), and check the degree-ten coefficient even when the small polynomial has no such term. The C++ self-tests additionally compare serial and parallel results around inversion-batch boundaries; Python and Java also test a deliberately small batch size.
Complexity Analysis
Let \(D=9\), let \(B=32768\) be the inversion-batch size, and let \(T\) be the number of workers, with \(T=1\) in a serial run. Each index requires \(D\) product-coefficient updates and a constant amount of other arithmetic; a full worker sum adds another \(D+1\) updates. Prefix and backward passes cost linear work in the batch size. The total number of batch inversions is at most \(\lceil N/B\rceil+T-1\), and testing the leading coefficient uses one additional inversion.
\[W(N,D,B,T,p)=O\!\left(ND+\left(\frac NB+T\right)\log p+TD^2\right).\]
The \(TD^2\) term accounts for the short polynomial convolutions at merges. With fixed \(D,B,T\) and this fixed modulus, total work is \(O(N)\). For the target \(N\), a serial run performs \(90{,}000{,}000\) product-coefficient updates and only \(306\) batch inversions, plus the final leading-coefficient inversion. Parallelism divides the main loop among workers, but does not change its total order of work; actual elapsed time also depends on scheduling and hardware.
Auxiliary storage is \(O(B+D)\) in the serial implementation and \(O(T(B+D))\) in the parallel implementation, excluding runtime thread stacks. There is no array indexed over all \(N\) nodes and no degree-\(N\) coefficient array. The same derivation can compute another positive degree \(r\) by retaining degrees \(0\) through \(r-1\); degree ten is what makes the present state only ten coefficients long.
Footnotes and References
- Problem page: Project Euler 1008 - Functional Inverse
- Polynomial interpolation: Wikipedia - Polynomial interpolation
- Newton form and divided differences: Wikipedia - Newton polynomial
- Finite fields: Wikipedia - Finite field
- Modular multiplicative inverses: Wikipedia - Modular multiplicative inverse
- Fermat's little theorem: Wikipedia - Fermat's little theorem
Problem 1008 source code
C++
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
namespace {
using u64 = std::uint64_t;
using Polynomial = std::array<u64, 10>;
constexpr u64 MOD = 1'000'000'007;
constexpr int TARGET = 10'000'000;
constexpr int BATCH_SIZE = 32'768;
u64 add(const u64 a, const u64 b) {
const u64 sum = a + b;
return sum >= MOD ? sum - MOD : sum;
}
u64 subtract(const u64 a, const u64 b) {
return a >= b ? a - b : a + MOD - b;
}
u64 multiply(const u64 a, const u64 b) {
return a * b % MOD;
}
u64 power(u64 base, u64 exponent) {
u64 result = 1;
while (exponent != 0) {
if ((exponent & 1) != 0) result = multiply(result, base);
base = multiply(base, base);
exponent >>= 1;
}
return result;
}
struct Block {
Polynomial product{1};
Polynomial sum{};
u64 factorial = 1;
};
template <bool FullSum>
Block calculate_block(const int begin, const int end) {
Block result;
std::vector<u64> inverse(BATCH_SIZE + 1);
for (int first = begin; first < end; first += BATCH_SIZE) {
const int count = std::min(BATCH_SIZE, end - first);
inverse[0] = 1;
for (int i = 1; i <= count; ++i) {
const u64 k = static_cast<u64>(first + i - 1);
inverse[i] = multiply(inverse[i - 1], multiply(k, 2 * k - 1));
}
u64 suffix_inverse = power(inverse[count], MOD - 2);
for (int i = count; i >= 1; --i) {
const u64 k = static_cast<u64>(first + i - 1);
const u64 weight = multiply(suffix_inverse, inverse[i - 1]);
suffix_inverse = multiply(suffix_inverse, multiply(k, 2 * k - 1));
inverse[i] = weight;
}
for (int i = 1; i <= count; ++i) {
const u64 k = static_cast<u64>(first + i - 1);
const u64 weight = inverse[i];
const u64 inverse_k = multiply(2 * k - 1, weight);
const u64 inverse_square = multiply(inverse_k, inverse_k);
result.factorial = multiply(result.factorial, k);
// P_N/x = sum A_(k-1)/(k(2k-1)), A_k = product_(j<=k)(1-x/j^2).
if constexpr (FullSum) {
for (int degree = 9; degree >= 1; --degree) {
result.sum[degree] = add(result.sum[degree],
multiply(weight, result.product[degree]));
result.product[degree] = subtract(result.product[degree],
multiply(inverse_square, result.product[degree - 1]));
}
result.sum[0] = add(result.sum[0], weight);
} else {
result.sum[9] = add(result.sum[9], multiply(weight, result.product[9]));
for (int degree = 9; degree >= 1; --degree) {
result.product[degree] = subtract(result.product[degree],
multiply(inverse_square, result.product[degree - 1]));
}
}
}
}
return result;
}
Polynomial product(const Polynomial& a, const Polynomial& b) {
Polynomial result{};
for (int i = 0; i <= 9; ++i) {
for (int j = 0; j <= i; ++j) {
result[i] = add(result[i], multiply(a[j], b[i - j]));
}
}
return result;
}
u64 solve(const int n, unsigned thread_count) {
thread_count = std::max(1U, std::min(thread_count, static_cast<unsigned>(n)));
Block total;
if (thread_count == 1) {
total = calculate_block<false>(1, n + 1);
} else {
std::vector<Block> blocks(thread_count);
std::vector<std::thread> workers;
for (unsigned index = 0; index < thread_count; ++index) {
const int begin = 1 + static_cast<int>(static_cast<u64>(n) * index / thread_count);
const int end = 1 + static_cast<int>(static_cast<u64>(n) * (index + 1) / thread_count);
workers.emplace_back([&, index, begin, end] {
blocks[index] = calculate_block<true>(begin, end);
});
}
for (auto& worker : workers) worker.join();
for (const Block& block : blocks) {
const Polynomial contribution = product(total.product, block.sum);
total.sum[9] = add(total.sum[9], contribution[9]);
total.product = product(total.product, block.product);
total.factorial = multiply(total.factorial, block.factorial);
}
}
const u64 factorial_square = multiply(total.factorial, total.factorial);
u64 leading = multiply(static_cast<u64>(n),
power(multiply(2ULL * n - 1, factorial_square), MOD - 2));
if (n % 2 == 0) leading = subtract(0, leading);
if (leading == 1) return total.sum[9];
// Otherwise the minimum monic polynomial is P_N + x*product_(k<=N)(x-k^2).
const u64 scale = n % 2 == 0 ? factorial_square : subtract(0, factorial_square);
return add(total.sum[9], multiply(scale, total.product[9]));
}
std::vector<u64> append_root(const std::vector<u64>& polynomial, const u64 root) {
std::vector<u64> result(polynomial.size() + 1);
for (std::size_t i = 0; i < polynomial.size(); ++i) {
result[i] = subtract(result[i], multiply(root, polynomial[i]));
result[i + 1] = add(result[i + 1], polynomial[i]);
}
return result;
}
u64 interpolate_directly(const int n) {
std::vector<u64> polynomial(static_cast<std::size_t>(n + 2));
for (int i = 1; i <= n; ++i) {
std::vector<u64> basis{1};
u64 denominator = 1;
for (int j = 0; j <= n; ++j) {
if (j == i) continue;
basis = append_root(basis, static_cast<u64>(j) * j);
denominator = multiply(denominator,
subtract(static_cast<u64>(i) * i, static_cast<u64>(j) * j));
}
const u64 scale = multiply(static_cast<u64>(i), power(denominator, MOD - 2));
for (std::size_t degree = 0; degree < basis.size(); ++degree) {
polynomial[degree] = add(polynomial[degree], multiply(scale, basis[degree]));
}
}
if (polynomial[n] != 1) {
std::vector<u64> vanishing{1};
for (int i = 0; i <= n; ++i) {
vanishing = append_root(vanishing, static_cast<u64>(i) * i);
}
for (std::size_t degree = 0; degree < vanishing.size(); ++degree) {
polynomial[degree] = add(polynomial[degree], vanishing[degree]);
}
}
return polynomial.size() > 10 ? polynomial[10] : 0;
}
void run_tests() {
for (int n = 1; n <= 40; ++n) {
const u64 expected = interpolate_directly(n);
if (solve(n, 1) != expected || solve(n, 3) != expected) {
throw std::runtime_error("Interpolation check failed for n=" + std::to_string(n));
}
}
for (const int n : {BATCH_SIZE - 1, BATCH_SIZE, BATCH_SIZE + 1, 2 * BATCH_SIZE + 3}) {
if (solve(n, 1) != solve(n, 3)) {
throw std::runtime_error("Batch boundary check failed for n=" + std::to_string(n));
}
}
std::cout << "All checks passed.\n";
}
} // namespace
int main(int argc, char* argv[]) {
try {
unsigned thread_count = std::min(16U, std::max(1U, std::thread::hardware_concurrency()));
if (argc == 2 && std::string(argv[1]) == "--self-test") {
run_tests();
return EXIT_SUCCESS;
}
if (argc == 3 && std::string(argv[1]) == "--threads") {
const std::string value = argv[2];
if (value.empty() || value.find_first_not_of("0123456789") != std::string::npos) {
throw std::invalid_argument("Thread count must be an integer from 1 to 128.");
}
const unsigned long parsed = std::stoul(value);
if (parsed < 1 || parsed > 128) {
throw std::invalid_argument("Thread count must be an integer from 1 to 128.");
}
thread_count = static_cast<unsigned>(parsed);
} else if (argc != 1) {
throw std::invalid_argument("Usage: Euler1008 [--threads COUNT | --self-test]");
}
std::cout << solve(TARGET, thread_count) << '\n';
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Python
#!/usr/bin/env python3
"""Project Euler Problem 1008 - Functional Inverse."""
from __future__ import annotations
MOD = 1_000_000_007
TARGET = 10_000_000
WANTED_DEGREE = 9 # P_N has a factor x, so x^10 comes from degree 9.
BATCH_SIZE = 32_768
def target_coefficient(n: int, batch_size: int = BATCH_SIZE) -> int:
"""Return the x^10 coefficient in the (n, MOD)-functional inverse.
If A_k(x) = product(1 - x/j^2, j=1..k), Newton interpolation gives
P_N(x) / x = sum(A_(k-1)(x) / (k(2k-1)), k=1..N).
Only coefficients through degree nine are retained. A batch inversion
supplies every 1/(k(2k-1)) with one modular exponentiation per batch.
"""
if n < 1 or 2 * n - 1 >= MOD:
raise ValueError("this implementation requires 1 <= n and 2n-1 < MOD")
if batch_size < 1:
raise ValueError("batch_size must be positive")
# coefficients[d] is [x^d] A_k for the current k.
coefficients = [0] * (WANTED_DEGREE + 1)
coefficients[0] = 1
interpolation_coefficient = 0
factorial = 1
prefix = [1] * (min(batch_size, n) + 1)
for first in range(1, n + 1, batch_size):
count = min(batch_size, n - first + 1)
# Prefix products of v_k = k(2k-1).
prefix[0] = 1
for index in range(1, count + 1):
k = first + index - 1
value = k * (2 * k - 1) % MOD
prefix[index] = prefix[index - 1] * value % MOD
# Recover every v_k^-1 by walking backward from the inverse of the
# complete product. Reuse prefix[] to store the resulting inverses.
suffix_inverse = pow(prefix[count], MOD - 2, MOD)
for index in range(count, 0, -1):
k = first + index - 1
value = k * (2 * k - 1) % MOD
weight = suffix_inverse * prefix[index - 1] % MOD
suffix_inverse = suffix_inverse * value % MOD
prefix[index] = weight
for index in range(1, count + 1):
k = first + index - 1
weight = prefix[index] # 1/(k(2k-1))
inverse_k = (2 * k - 1) * weight % MOD
inverse_square = inverse_k * inverse_k % MOD
# The summand uses A_(k-1), so accumulate before updating A.
interpolation_coefficient = (
interpolation_coefficient
+ weight * coefficients[WANTED_DEGREE]
) % MOD
for degree in range(WANTED_DEGREE, 0, -1):
coefficients[degree] = (
coefficients[degree]
- inverse_square * coefficients[degree - 1]
) % MOD
factorial = factorial * k % MOD
factorial_square = factorial * factorial % MOD
leading = n * pow(
(2 * n - 1) * factorial_square % MOD, MOD - 2, MOD
) % MOD
if n % 2 == 0:
leading = -leading % MOD
# P_N is already the minimum monic polynomial only when its leading
# coefficient is one. Otherwise add the monic vanishing polynomial
# x*product(x-k^2, k=1..N).
if leading == 1:
return interpolation_coefficient
vanishing_scale = factorial_square if n % 2 == 0 else -factorial_square % MOD
return (
interpolation_coefficient
+ vanishing_scale * coefficients[WANTED_DEGREE]
) % MOD
def append_root(polynomial: list[int], root: int) -> list[int]:
"""Return polynomial * (x-root), with coefficients modulo MOD."""
result = [0] * (len(polynomial) + 1)
for degree, coefficient in enumerate(polynomial):
result[degree] = (result[degree] - root * coefficient) % MOD
result[degree + 1] = (result[degree + 1] + coefficient) % MOD
return result
def interpolate_directly(n: int) -> int:
"""Build the whole small polynomial by Lagrange interpolation.
This cubic-time reference is intentionally independent of the truncated
Newton recurrence and is used only by the checkpoints below.
"""
polynomial = [0] * (n + 2)
for i in range(1, n + 1):
basis = [1]
denominator = 1
node_i = i * i % MOD
for j in range(n + 1):
if j == i:
continue
node_j = j * j % MOD
basis = append_root(basis, node_j)
denominator = denominator * (node_i - node_j) % MOD
scale = i * pow(denominator, MOD - 2, MOD) % MOD
for degree, coefficient in enumerate(basis):
polynomial[degree] = (
polynomial[degree] + scale * coefficient
) % MOD
if polynomial[n] != 1:
vanishing = [1]
for i in range(n + 1):
vanishing = append_root(vanishing, i * i % MOD)
for degree, coefficient in enumerate(vanishing):
polynomial[degree] = (polynomial[degree] + coefficient) % MOD
return polynomial[10] if len(polynomial) > 10 else 0
def run_checkpoints() -> None:
for n in range(1, 41):
expected = interpolate_directly(n)
assert target_coefficient(n) == expected, f"interpolation check for n={n}"
assert target_coefficient(n, 7) == expected, f"batch check for n={n}"
def main() -> None:
run_checkpoints()
print(target_coefficient(TARGET))
if __name__ == "__main__":
main()
Java
public class Euler1008 {
private static final long MOD = 1_000_000_007L;
private static final int TARGET = 10_000_000;
// P_N has a factor x, so its x^10 coefficient comes from degree nine.
private static final int WANTED_DEGREE = 9;
private static final int BATCH_SIZE = 32_768;
private static long add(long lhs, long rhs) {
long sum = lhs + rhs;
return sum >= MOD ? sum - MOD : sum;
}
private static long subtract(long lhs, long rhs) {
return lhs >= rhs ? lhs - rhs : lhs + MOD - rhs;
}
private static long multiply(long lhs, long rhs) {
// Both operands are below MOD, so their product fits in signed long.
return lhs * rhs % MOD;
}
private static long modPow(long base, long exponent) {
long result = 1;
while (exponent != 0) {
if ((exponent & 1L) != 0) {
result = multiply(result, base);
}
base = multiply(base, base);
exponent >>= 1;
}
return result;
}
private static long targetCoefficient(int n) {
return targetCoefficient(n, BATCH_SIZE);
}
private static long targetCoefficient(int n, int batchSize) {
if (n < 1 || 2L * n - 1 >= MOD) {
throw new IllegalArgumentException(
"this implementation requires 1 <= n and 2n-1 < MOD");
}
if (batchSize < 1) {
throw new IllegalArgumentException("batchSize must be positive");
}
// coefficients[d] is [x^d] A_k, where
// A_k(x) = product(1 - x/j^2, j=1..k).
long[] coefficients = new long[WANTED_DEGREE + 1];
coefficients[0] = 1;
long interpolationCoefficient = 0;
long factorial = 1;
long[] prefix = new long[Math.min(batchSize, n) + 1];
for (int first = 1; first <= n; first += batchSize) {
int count = Math.min(batchSize, n - first + 1);
// Prefix products of v_k = k(2k-1).
prefix[0] = 1;
for (int index = 1; index <= count; ++index) {
long k = (long) first + index - 1;
long value = multiply(k % MOD, (2 * k - 1) % MOD);
prefix[index] = multiply(prefix[index - 1], value);
}
// One Fermat inverse followed by a backward pass supplies every
// 1/v_k in the batch. Store those weights back into prefix[].
long suffixInverse = modPow(prefix[count], MOD - 2);
for (int index = count; index >= 1; --index) {
long k = (long) first + index - 1;
long value = multiply(k % MOD, (2 * k - 1) % MOD);
long weight = multiply(suffixInverse, prefix[index - 1]);
suffixInverse = multiply(suffixInverse, value);
prefix[index] = weight;
}
for (int index = 1; index <= count; ++index) {
long k = (long) first + index - 1;
long weight = prefix[index]; // 1/(k(2k-1))
long inverseK = multiply((2 * k - 1) % MOD, weight);
long inverseSquare = multiply(inverseK, inverseK);
// Newton interpolation gives
// P_N/x = sum A_(k-1)/(k(2k-1)). Accumulate the summand
// before changing A_(k-1) into A_k.
interpolationCoefficient = add(
interpolationCoefficient,
multiply(weight, coefficients[WANTED_DEGREE]));
for (int degree = WANTED_DEGREE; degree >= 1; --degree) {
coefficients[degree] = subtract(
coefficients[degree],
multiply(inverseSquare, coefficients[degree - 1]));
}
factorial = multiply(factorial, k % MOD);
}
}
long factorialSquare = multiply(factorial, factorial);
long leading = multiply(
n,
modPow(multiply((2L * n - 1) % MOD, factorialSquare), MOD - 2));
if ((n & 1) == 0) {
leading = subtract(0, leading);
}
// If P_N is not monic, the lowest-degree monic solution is obtained
// by adding x*product(x-k^2, k=1..N).
if (leading == 1) {
return interpolationCoefficient;
}
long vanishingScale = (n & 1) == 0
? factorialSquare
: subtract(0, factorialSquare);
return add(
interpolationCoefficient,
multiply(vanishingScale, coefficients[WANTED_DEGREE]));
}
private static long[] appendRoot(long[] polynomial, long root) {
long[] result = new long[polynomial.length + 1];
for (int degree = 0; degree < polynomial.length; ++degree) {
result[degree] = subtract(
result[degree], multiply(root, polynomial[degree]));
result[degree + 1] = add(result[degree + 1], polynomial[degree]);
}
return result;
}
// Independent cubic-time Lagrange interpolation for small checkpoints.
private static long interpolateDirectly(int n) {
long[] polynomial = new long[n + 2];
for (int i = 1; i <= n; ++i) {
long[] basis = {1};
long denominator = 1;
long nodeI = (long) i * i % MOD;
for (int j = 0; j <= n; ++j) {
if (j == i) {
continue;
}
long nodeJ = (long) j * j % MOD;
basis = appendRoot(basis, nodeJ);
denominator = multiply(denominator, subtract(nodeI, nodeJ));
}
long scale = multiply(i, modPow(denominator, MOD - 2));
for (int degree = 0; degree < basis.length; ++degree) {
polynomial[degree] = add(
polynomial[degree], multiply(scale, basis[degree]));
}
}
if (polynomial[n] != 1) {
long[] vanishing = {1};
for (int i = 0; i <= n; ++i) {
vanishing = appendRoot(vanishing, (long) i * i % MOD);
}
for (int degree = 0; degree < vanishing.length; ++degree) {
polynomial[degree] = add(polynomial[degree], vanishing[degree]);
}
}
return polynomial.length > 10 ? polynomial[10] : 0;
}
private static void requireCheckpoint(boolean condition, String description) {
if (!condition) {
throw new AssertionError("checkpoint failed: " + description);
}
}
private static void runCheckpoints() {
for (int n = 1; n <= 40; ++n) {
long expected = interpolateDirectly(n);
requireCheckpoint(
targetCoefficient(n) == expected,
"interpolation check for n=" + n);
requireCheckpoint(
targetCoefficient(n, 7) == expected,
"batch check for n=" + n);
}
}
public static void main(String[] args) {
runCheckpoints();
System.out.println(targetCoefficient(TARGET));
}
}