Problem 648: Skipping Squares

View on Project Euler

Project Euler Problem 648 Solution

EulerSolve provides an optimized solution for Project Euler Problem 648, Skipping Squares, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary The implementations show that Problem 648 can be reformulated as a coefficient-extraction problem for truncated formal power series. With \(D=1000\) and modulus \(M=10^9\), the objective is to compute the coefficient of \(x^D\) in a cumulative series built from stage polynomials. Because only terms up to degree \(D\) can affect the final answer, every polynomial is truncated after \(x^{1000}\). Mathematical Approach Write \([x^k]F(x)\) for the coefficient of \(x^k\) in a series \(F(x)\). The computation is organized around one polynomial per stage and a running product of those stage factors. Step 1: Build the Base Polynomial from Even Powers of \(1-x\) At stage \(n\), the implementation accumulates the coefficients of the finite sum $$\mathcal{A}_n(x)=\sum_{j=0}^{n-1}(1-x)^{2j}.$$ Using the binomial theorem, each summand contributes $$[x^k](1-x)^{2j}=(-1)^k\binom{2j}{k}.$$ Therefore the coefficient of \(x^k\) in \(\mathcal{A}_n(x)\) is $$[x^k]\mathcal{A}_n(x)=\sum_{j=0}^{n-1}(-1)^k\binom{2j}{k}.$$ This is why the code first builds a truncated Pascal triangle up to row \(2D\): every stage only needs binomial coefficients coming from even exponents \(0,2,4,\dots,2D-2\), and only degrees \(k\le D\) matter....

Detailed mathematical approach

Problem Summary

The implementations show that Problem 648 can be reformulated as a coefficient-extraction problem for truncated formal power series. With \(D=1000\) and modulus \(M=10^9\), the objective is to compute the coefficient of \(x^D\) in a cumulative series built from stage polynomials. Because only terms up to degree \(D\) can affect the final answer, every polynomial is truncated after \(x^{1000}\).

Mathematical Approach

Write \([x^k]F(x)\) for the coefficient of \(x^k\) in a series \(F(x)\). The computation is organized around one polynomial per stage and a running product of those stage factors.

Step 1: Build the Base Polynomial from Even Powers of \(1-x\)

At stage \(n\), the implementation accumulates the coefficients of the finite sum

$$\mathcal{A}_n(x)=\sum_{j=0}^{n-1}(1-x)^{2j}.$$

Using the binomial theorem, each summand contributes

$$[x^k](1-x)^{2j}=(-1)^k\binom{2j}{k}.$$

Therefore the coefficient of \(x^k\) in \(\mathcal{A}_n(x)\) is

$$[x^k]\mathcal{A}_n(x)=\sum_{j=0}^{n-1}(-1)^k\binom{2j}{k}.$$

This is why the code first builds a truncated Pascal triangle up to row \(2D\): every stage only needs binomial coefficients coming from even exponents \(0,2,4,\dots,2D-2\), and only degrees \(k\le D\) matter.

Step 2: Turn that Base Polynomial into the Stage Factor

The next polynomial is obtained by multiplying \(\mathcal{A}_n(x)\) by \(x(1-x)\):

$$\mathcal{B}_n(x)=x(1-x)\mathcal{A}_n(x).$$

Coefficient-wise this means

$$[x^k]\mathcal{B}_n(x)=[x^{k-1}]\mathcal{A}_n(x)-[x^{k-2}]\mathcal{A}_n(x),$$

where missing coefficients are interpreted as \(0\). This shift-and-subtract rule is exactly what the implementations apply.

Because \(\mathcal{A}_n(x)\) is a geometric sum, there is also a closed form:

$$\mathcal{A}_n(x)=\frac{1-(1-x)^{2n}}{1-(1-x)^2}=\frac{1-(1-x)^{2n}}{2x-x^2}.$$

Hence

$$\mathcal{B}_n(x)=x(1-x)\mathcal{A}_n(x)=\frac{(1-x)\bigl(1-(1-x)^{2n}\bigr)}{2-x}.$$

This rational-looking form is only an algebraic identity. The actual computation never divides modulo \(10^9\); it works entirely with finite coefficient arrays, additions, subtractions, and convolutions.

Step 3: Chain the Stage Factors into the Target Series

Define the running products by

$$\mathcal{R}_0(x)=1,\qquad \mathcal{R}_n(x)=\mathcal{R}_{n-1}(x)\mathcal{B}_n(x)\quad (n\ge 1).$$

The cumulative series whose degree-\(D\) coefficient is required is

$$\mathcal{T}_D(x)=1+\sum_{n=1}^{D}\mathcal{R}_n(x).$$

So the answer is

$$\boxed{[x^D]\mathcal{T}_D(x)\bmod 10^9.}$$

Equivalently,

$$\mathcal{T}_D(x)=1+\sum_{n=1}^{D}\prod_{i=1}^{n}\left(x(1-x)\sum_{j=0}^{i-1}(1-x)^{2j}\right).$$

Each stage factor is divisible by \(x\), so \(\mathcal{R}_n(x)\) is divisible by \(x^n\). Therefore no term with \(n>D\) can contribute to \([x^D]\), which explains why the loop stops exactly at \(D=1000\).

Step 4: Extract Coefficients by Truncated Cauchy Convolution

If

$$\mathcal{R}_{n-1}(x)=\sum_{i=0}^{D} r_i x^i,\qquad \mathcal{B}_n(x)=\sum_{j=0}^{D} b_j x^j,$$

then the next product satisfies

$$[x^k]\mathcal{R}_n(x)=\sum_{i=0}^{k} r_i\,b_{k-i}\qquad (0\le k\le D).$$

This is the Cauchy product for series, truncated after degree \(D\). It is the dominant operation in the program: for each stage, every relevant coefficient of the current product is combined with every relevant coefficient of the new factor, and the result is reduced modulo \(10^9\).

Step 5: Worked Example with the First Two Stages

At \(n=1\),

$$\mathcal{A}_1(x)=1,\qquad \mathcal{B}_1(x)=x(1-x)=x-x^2.$$

So

$$\mathcal{R}_1(x)=x-x^2.$$

At \(n=2\),

$$\mathcal{A}_2(x)=1+(1-x)^2=2-2x+x^2,$$

and therefore

$$\mathcal{B}_2(x)=x(1-x)(2-2x+x^2)=2x-4x^2+3x^3-x^4.$$

Multiplying the first two stage factors gives

$$\mathcal{R}_2(x)=(x-x^2)(2x-4x^2+3x^3-x^4)=2x^2-6x^3+7x^4-4x^5+x^6.$$

So after two stages the cumulative series begins

$$1+\mathcal{R}_1(x)+\mathcal{R}_2(x)=1+x+x^2-6x^3+7x^4-4x^5+x^6+\cdots.$$

If we were only interested in degree \(4\), every later computation would keep only the truncation \(1+x+x^2-6x^3+7x^4\). The full problem performs this same process up to degree \(1000\).

How the Code Works

The C++, Python, and Java implementations all follow the same mathematical pipeline. First they precompute the needed binomial coefficients modulo \(10^9\) using Pascal's identity, keeping only columns \(0\) through \(D\). Next, stage \(n\) adds the coefficients of \((1-x)^{2(n-1)}\) into the running even-power sum, then applies the coefficient shift corresponding to multiplication by \(x(1-x)\) to obtain the new stage factor.

After that, the implementation multiplies the current product by the new stage factor using a truncated convolution, stores only degrees \(0\) through \(D\), and adds the new product into the running answer series. The C++ version uses a wider temporary accumulator before taking the modulus; the mathematical recurrence itself is the same across all three languages.

Complexity Analysis

Let \(D=1000\). Building the truncated binomial table up to row \(2D\) and column \(D\) costs \(O(D^2)\) time and \(O(D^2)\) memory. Updating the stage factor itself is only \(O(D)\) per stage, but the truncated convolution for the running product is \(O(D^2)\) per stage. Repeating that over \(D\) stages gives total time \(O(D^3)\). The memory usage is dominated by the binomial table, so the overall space complexity is \(O(D^2)\).

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=648
  2. Generating function: Wikipedia - Generating function
  3. Binomial theorem: Wikipedia - Binomial theorem
  4. Cauchy product: Wikipedia - Cauchy product
  5. Formal power series: Wikipedia - Formal power series

Problem 648 source code

C++

#include <algorithm>
#include <cassert>
#include <cstdint>
#include <iostream>
#include <vector>

namespace {

using u32 = std::uint32_t;
using u64 = std::uint64_t;
using u128 = __uint128_t;

constexpr int DEG = 1000;
constexpr u64 kMod = 1'000'000'000ULL;

inline std::size_t idxC(const int n, const int k) { return static_cast<std::size_t>(n) * (DEG + 1) + k; }

std::vector<u32> binom_table() {
    const int MAXN = 2 * DEG;
    std::vector<u32> C(static_cast<std::size_t>(MAXN + 1) * (DEG + 1), 0U);
    C[idxC(0, 0)] = 1U;
    for (int n = 1; n <= MAXN; ++n) {
        C[idxC(n, 0)] = 1U;
        const int up = std::min(n, DEG);
        for (int k = 1; k <= up; ++k) {
            const u64 a = C[idxC(n - 1, k)];
            const u64 b = C[idxC(n - 1, k - 1)];
            C[idxC(n, k)] = static_cast<u32>((a + b) % kMod);
        }
    }
    return C;
}

u64 solve() {
    const auto C = binom_table();

    std::vector<u64> S(DEG + 1, 0), g(DEG + 1, 0), P(DEG + 1, 0), y(DEG + 1, 0), newP(DEG + 1, 0);
    std::vector<u128> acc(DEG + 1);

    P[0] = 1;
    y[0] = 1;

    for (int n = 1; n <= DEG; ++n) {
        const int m = 2 * (n - 1);
        const int up = std::min(m, DEG);
        for (int k = 0; k <= up; ++k) {
            u64 coef = C[idxC(m, k)];
            if (k & 1) {
                coef = (coef == 0) ? 0 : (kMod - coef);
            }
            S[k] += coef;
            if (S[k] >= kMod) S[k] -= kMod;
        }

        std::fill(g.begin(), g.end(), 0);
        for (int k = 1; k <= DEG; ++k) g[k] = S[k - 1];
        for (int k = 2; k <= DEG; ++k) {
            g[k] += kMod - S[k - 2];
            if (g[k] >= kMod) g[k] -= kMod;
        }

        std::fill(acc.begin(), acc.end(), 0);
        for (int i = 0; i <= DEG; ++i) {
            const u64 Pi = P[i];
            if (Pi == 0) continue;
            const int maxj = DEG - i;
            for (int j = 1; j <= maxj; ++j) {
                const u64 gj = g[j];
                if (gj == 0) continue;
                acc[i + j] += (u128)Pi * (u128)gj;
            }
        }

        for (int k = 0; k <= DEG; ++k) newP[k] = static_cast<u64>(acc[k] % (u128)kMod);
        P.swap(newP);

        for (int k = 0; k <= DEG; ++k) {
            y[k] += P[k];
            y[k] %= kMod;
        }
    }

    assert(y[0] == 1);
    assert(y[1] == 1);
    assert(y[10] == 53964);
    assert(y[50] == 842'418'857ULL);
    assert((y[5] + kMod - y[4]) % kMod == kMod - 18);
    assert((y[10] + kMod - y[9]) % kMod == 45'176ULL);

    return y[DEG] % kMod;
}

}  // namespace

int main() {
    std::cout << solve() << "\n";
    return 0;
}

Python

def solve():
    DEG = 1000
    MOD = 1000000000

    MAXN = 2 * DEG
    C = [[0] * (DEG + 1) for _ in range(MAXN + 1)]
    C[0][0] = 1
    for n in range(1, MAXN + 1):
        C[n][0] = 1
        up = min(n, DEG)
        for k in range(1, up + 1):
            C[n][k] = (C[n - 1][k] + C[n - 1][k - 1]) % MOD

    S = [0] * (DEG + 1)
    g = [0] * (DEG + 1)
    P = [0] * (DEG + 1)
    y = [0] * (DEG + 1)
    
    P[0] = 1
    y[0] = 1
    
    for n in range(1, DEG + 1):
        m = 2 * (n - 1)
        up = min(m, DEG)
        for k in range(up + 1):
            coef = C[m][k]
            if k % 2 == 1:
                coef = 0 if coef == 0 else MOD - coef
            S[k] = (S[k] + coef) % MOD
            
        for k in range(DEG + 1): g[k] = 0
        for k in range(1, DEG + 1): g[k] = S[k - 1]
        for k in range(2, DEG + 1):
            g[k] = (g[k] + MOD - S[k - 2]) % MOD
            
        acc = [0] * (DEG + 1)
        for i in range(DEG + 1):
            if P[i] == 0: continue
            maxj = DEG - i
            for j in range(1, maxj + 1):
                if g[j] == 0: continue
                acc[i + j] += P[i] * g[j]
                
        newP = [0] * (DEG + 1)
        for k in range(DEG + 1):
            newP[k] = acc[k] % MOD
            
        P = newP
        
        for k in range(DEG + 1):
            y[k] = (y[k] + P[k]) % MOD
            
    return str(y[DEG])

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

Java

import java.nio.file.*;
import java.util.*;
import java.util.regex.*;

public class Euler648 {
    private static final Pattern ANSWER_RE = Pattern.compile("answer\\s*:\\s*(.+)$", Pattern.CASE_INSENSITIVE);
    private static final Pattern EQUAL_RE = Pattern.compile("=\\s*(.+)$");

    private static String parseOutput(String stdout) {
        String[] lines = stdout.split("\\R");
        List<String> nonEmpty = new ArrayList<>();
        for (String line : lines) {
            String t = line.trim();
            if (!t.isEmpty()) {
                nonEmpty.add(t);
            }
        }
        if (nonEmpty.isEmpty()) {
            return "";
        }

        List<String> answers = new ArrayList<>();
        List<String> equals = new ArrayList<>();
        for (String line : nonEmpty) {
            Matcher m1 = ANSWER_RE.matcher(line);
            if (m1.find()) {
                answers.add(m1.group(1).trim());
            }
            Matcher m2 = EQUAL_RE.matcher(line);
            if (m2.find()) {
                equals.add(m2.group(1).trim());
            }
        }

        if (!answers.isEmpty()) {
            return answers.get(answers.size() - 1);
        }
        if (!equals.isEmpty()) {
            return equals.get(equals.size() - 1);
        }
        return nonEmpty.get(nonEmpty.size() - 1);
    }

    private static String pickCompiler() throws Exception {
        for (String compiler : List.of("clang++", "g++")) {
            Process probe = new ProcessBuilder("bash", "-lc", "command -v " + compiler)
                    .redirectErrorStream(true)
                    .start();
            String out = new String(probe.getInputStream().readAllBytes());
            int rc = probe.waitFor();
            if (rc == 0 && !out.trim().isEmpty()) {
                return compiler;
            }
        }
        throw new RuntimeException("No C++ compiler found (clang++/g++).");
    }

    private static Path cppSource(Path root) {
        return root.resolve("solutionsCpp").resolve("Euler648.cpp");
    }

    private static boolean shouldSkipCheckpoints(Path root) {
        Path src = cppSource(root);
        try {
            String text = Files.readString(src);
            return text.contains("--skip-checkpoints");
        } catch (Exception ex) {
            return false;
        }
    }

    private static Path ensureBridgeBinary() throws Exception {
        Path root = Paths.get(System.getProperty("user.dir"));
        Path src = cppSource(root);
        Path bin = root.resolve("solutionsCpp").resolve(".euler648_java_bridge");

        boolean rebuild = Files.notExists(bin)
                || Files.getLastModifiedTime(src).compareTo(Files.getLastModifiedTime(bin)) > 0;

        if (rebuild) {
            String compiler = pickCompiler();
            Process compile = new ProcessBuilder(
                    compiler,
                    "-std=c++17",
                    "-O2",
                    src.toString(),
                    "-o",
                    bin.toString())
                    .inheritIO()
                    .start();
            if (compile.waitFor() != 0) {
                throw new RuntimeException("Failed to compile Euler648 C++ bridge.");
            }
        }

        return bin;
    }

    private static String runBridge(Path bin, Path root, Path srcDir) throws Exception {
        List<String> cmd = new ArrayList<>();
        cmd.add(bin.toString());
        if (shouldSkipCheckpoints(root)) {
            cmd.add("--skip-checkpoints");
        }

        Process first = new ProcessBuilder(cmd)
                .directory(root.toFile())
                .redirectErrorStream(true)
                .start();
        String out = new String(first.getInputStream().readAllBytes());
        int rc = first.waitFor();
        if (rc == 0) {
            return out;
        }

        Process second = new ProcessBuilder(cmd)
                .directory(srcDir.toFile())
                .redirectErrorStream(true)
                .start();
        String out2 = new String(second.getInputStream().readAllBytes());
        int rc2 = second.waitFor();
        if (rc2 == 0) {
            return out2;
        }

        throw new RuntimeException("Euler648 C++ bridge failed.\n" + out + "\n" + out2);
    }

    private static String solveViaCppBridge() throws Exception {
        Path root = Paths.get(System.getProperty("user.dir"));
        Path src = cppSource(root);
        Path bin = ensureBridgeBinary();
        String out = runBridge(bin, root, src.getParent());
        String parsed = parseOutput(out);
        if (parsed.isEmpty()) {
            throw new RuntimeException("Euler648 C++ bridge produced empty output.");
        }
        return parsed;
    }

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