Problem 438: Integer Part of Polynomial Equation's Solutions

View on Project Euler

Project Euler Problem 438 Solution

EulerSolve provides an optimized solution for Project Euler Problem 438, Integer Part of Polynomial Equation's Solutions, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary We consider monic degree-\(7\) polynomials $$P(t)=t^7+a_1t^6+a_2t^5+a_3t^4+a_4t^3+a_5t^2+a_6t+a_7$$ with integer coefficients, all roots real, and sorted roots \(r_1\le \cdots \le r_7\) satisfying $$\lfloor r_i\rfloor=i \qquad (1\le i\le 7).$$ For every admissible coefficient vector \(a=(a_1,\dots,a_7)\), define $$S(a)=\sum_{i=1}^7 |a_i|.$$ The goal is to sum \(S(a)\) over all such polynomials. Mathematical Approach Step 1: Rewrite the polynomial through its roots Because the roots are real and lie in the positive intervals \([1,2),[2,3),\dots,[7,8)\), we may write $$P(t)=\prod_{i=1}^7 (t-r_i)=t^7-e_1t^6+e_2t^5-e_3t^4+e_4t^3-e_5t^2+e_6t-e_7,$$ where \(e_m\) is the \(m\)-th elementary symmetric sum of the roots. Therefore $$a_m=(-1)^m e_m \qquad (1\le m\le 7).$$ Since every root is positive, all \(e_m\) are positive as well, and the objective becomes simply $$S(a)=e_1+e_2+\cdots+e_7.$$ So the search is really over the positive symmetric sums \(e_1,\dots,e_7\), not over arbitrary signed coefficients....

Detailed mathematical approach

Problem Summary

We consider monic degree-\(7\) polynomials

$$P(t)=t^7+a_1t^6+a_2t^5+a_3t^4+a_4t^3+a_5t^2+a_6t+a_7$$

with integer coefficients, all roots real, and sorted roots \(r_1\le \cdots \le r_7\) satisfying

$$\lfloor r_i\rfloor=i \qquad (1\le i\le 7).$$

For every admissible coefficient vector \(a=(a_1,\dots,a_7)\), define

$$S(a)=\sum_{i=1}^7 |a_i|.$$

The goal is to sum \(S(a)\) over all such polynomials.

Mathematical Approach

Step 1: Rewrite the polynomial through its roots

Because the roots are real and lie in the positive intervals \([1,2),[2,3),\dots,[7,8)\), we may write

$$P(t)=\prod_{i=1}^7 (t-r_i)=t^7-e_1t^6+e_2t^5-e_3t^4+e_4t^3-e_5t^2+e_6t-e_7,$$

where \(e_m\) is the \(m\)-th elementary symmetric sum of the roots. Therefore

$$a_m=(-1)^m e_m \qquad (1\le m\le 7).$$

Since every root is positive, all \(e_m\) are positive as well, and the objective becomes simply

$$S(a)=e_1+e_2+\cdots+e_7.$$

So the search is really over the positive symmetric sums \(e_1,\dots,e_7\), not over arbitrary signed coefficients.

Step 2: Immediate bounds from the root intervals

For the elementary symmetric polynomial

$$E_m(b_1,\dots,b_7)=\sum_{1\le i_1<\cdots<i_m\le 7} b_{i_1}\cdots b_{i_m},$$

monotonicity on positive inputs gives

$$E_m(1,2,\dots,7)\le e_m < E_m(2,3,\dots,8).$$

Evaluating these endpoint sums yields the integer coefficient box

$$\begin{aligned} 28\le e_1\le 34,\qquad &322\le e_2\le 510,\qquad 1960\le e_3\le 4024,\\ 6769\le e_4\le 18423,\qquad &13132\le e_5\le 48859,\\ 13068\le e_6\le 69263,\qquad &5040\le e_7\le 40319. \end{aligned}$$

These bounds already remove the overwhelming majority of impossible tuples.

Step 3: Convert the floor condition into sign conditions

For an integer \(k\in\{1,\dots,8\}\), the value

$$P(k)=\prod_{i=1}^7 (k-r_i)$$

has a prescribed sign. Indeed, when \(P(k)\neq 0\), exactly \(8-k\) factors are negative, so

$$(-1)^k P(k)\ge 0 \qquad (1\le k\le 8).$$

Written in the symmetric sums, this becomes the affine inequality

$$(-1)^k\left(k^7-e_1k^6+e_2k^5-e_3k^4+e_4k^3-e_5k^2+e_6k-e_7\right)\ge 0.$$

If all eight inequalities are strict, then the sign alternates between consecutive integers, so the intermediate value theorem forces one root in each open interval \((k,k+1)\). Since the degree is \(7\), that already accounts for all roots.

Step 4: Boundary roots require a derivative check

Equality \(P(k)=0\) means a root lies exactly on an integer boundary. The sign test alone is not enough there: the polynomial must cross the axis in the direction compatible with the interval \([k,k+1)\).

Near a simple root at \(k\),

$$P(t)\approx P'(k)(t-k),$$

so the admissible orientation is

$$(-1)^{k+1}P'(k)>0 \qquad (1\le k\le 7).$$

At \(k=8\), equality is impossible for a valid polynomial, because no root may have integer part \(8\). This is exactly why the implementation performs a final endpoint validation after the linear range computation.

Step 5: The admissible region is linear

We now have two kinds of constraints:

$$\text{box bounds for } e_1,\dots,e_7,$$

and

$$8 \text{ affine sign constraints coming from } P(1),P(2),\dots,P(8).$$

Every one of these conditions is linear in the variables \(e_1,\dots,e_7\). Therefore the feasible set is a polyhedral region inside the coefficient box.

Step 6: Eliminate later variables before the search

The implementation applies Fourier-Motzkin elimination to project away the later coefficients one by one. After these projections have been precomputed, fixing the first few coefficients immediately produces a much tighter lower and upper bound for the next one.

So the search does not walk through the entire raw box. It chooses coefficients in order, repeatedly intersects the static interval from Step 2 with the projected linear interval coming from the remaining sign conditions, and abandons the branch as soon as the interval becomes empty.

Step 7: Why the last coefficient is summed as an interval

After \(e_1,\dots,e_6\) have been chosen, the remaining valid values of \(e_7\) form a single integer interval \([L,R]\). Because all constraints are linear in \(e_7\), any equality case \(P(k)=0\) can only occur at the ends of that interval, so only the endpoints need the derivative test from Step 4.

Once the valid interval is known, its total contribution to the objective is

$$\sum_{u=L}^{R}(e_1+\cdots+e_6+u)=(R-L+1)(e_1+\cdots+e_6)+\frac{(L+R)(R-L+1)}{2}.$$

This arithmetic-series formula is what lets the implementation aggregate an entire final interval in constant time.

How the Code Works

The C++, Python, and Java implementations use the same mathematical pipeline. They enumerate the positive symmetric sums instead of the signed coefficients, start from the interval bounds implied by the root locations, precompute projected linear constraints, and then run a depth-first search whose branches are cut as soon as a coefficient interval is empty. For the final coefficient, they validate only boundary values that make some \(P(k)\) vanish, and then add the whole interval contribution with the closed formula above rather than iterating one integer at a time.

Complexity Analysis

Brute force over the coefficient box would be astronomically large. The implemented method is still exponential in the number of free coefficients in the worst case, because it is fundamentally a search problem, but the projected linear bounds reduce the branching factor dramatically. Memory usage stays small: the algorithm stores only a few projected linear systems, the current recursion stack, and the running total.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=438
  2. Vieta's formulas: Wikipedia — Vieta's formulas
  3. Elementary symmetric polynomial: Wikipedia — Elementary symmetric polynomial
  4. Fourier-Motzkin elimination: Wikipedia — Fourier-Motzkin elimination
  5. Intermediate value theorem: Wikipedia — Intermediate value theorem

Problem 438 source code

C++

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <utility>
#include <vector>

namespace {

using i64 = long long;
using i128 = __int128_t;
using Vec = std::vector<double>;
using Mat = std::vector<Vec>;

constexpr double kEps = 1e-9;
constexpr int kN = 7;
constexpr int kM = 2;
constexpr int kLower[kN + 1] = {1, 28, 322, 1960, 6769, 13132, 13068, 5040};
constexpr int kUpper[kN + 1] = {1, 35, 511, 4025, 18424, 48860, 69264, 40320};

struct System {
    Mat lower;
    Mat upper;
    Mat equal;

    void add_constraint(Vec c) {
        double coef = -c.back();
        c.pop_back();
        if (std::abs(coef) < kEps) {
            equal.push_back(std::move(c));
            return;
        }
        for (double& v : c) v /= coef;
        if (coef > kEps) {
            lower.push_back(std::move(c));
        } else {
            upper.push_back(std::move(c));
        }
    }
};

Vec diff_vec(const Vec& a, const Vec& b) {
    Vec c(a.size());
    for (std::size_t i = 0; i < a.size(); ++i) c[i] = a[i] - b[i];
    return c;
}

System eliminate_one(const System& s) {
    System out;
    for (const Vec& e : s.equal) out.add_constraint(e);
    for (const Vec& lo : s.lower) {
        for (const Vec& hi : s.upper) {
            out.add_constraint(diff_vec(lo, hi));
        }
    }
    return out;
}

double eval_linear(const Vec& a, const std::vector<int>& x) {
    double v = a[0];
    for (std::size_t i = 0; i < x.size(); ++i) v += a[i + 1] * static_cast<double>(x[i]);
    return v;
}

int tight_floor(const double x) {
    return static_cast<int>(std::floor(x + kEps));
}

int tight_ceil(const double x) {
    return static_cast<int>(std::ceil(x - kEps));
}

std::pair<int, int> infer_range(const System& s, const std::vector<int>& x) {
    for (const Vec& e : s.equal) {
        if (eval_linear(e, x) > kEps) return {1, 0};
    }
    int l = -1000000000;
    int r = 1000000000;
    for (const Vec& lo : s.lower) l = std::max(l, tight_ceil(eval_linear(lo, x)));
    for (const Vec& hi : s.upper) r = std::min(r, tight_floor(eval_linear(hi, x)));
    return {l, r};
}

bool valid_endpoint(std::vector<int> x, const int last) {
    x.push_back(last);
    for (int k = 1; k <= kN + 1; ++k) {
        int val = 1;
        for (int i = 0; i < kN; ++i) {
            val = k * val + ((i % 2 == 1) ? x[i] : -x[i]);
        }
        if (val == 0) {
            int der = kN;
            for (int i = 0; i < kN - 1; ++i) {
                der = k * der + (kN - i - 1) * ((i % 2 == 1) ? x[i] : -x[i]);
            }
            if (((kN + k) % 2 == 1) ? (der >= 0) : (der <= 0)) return false;
        }
    }
    return true;
}

std::string to_string_i128(i128 v) {
    if (v == 0) return "0";
    bool neg = (v < 0);
    if (neg) v = -v;
    std::string s;
    while (v > 0) {
        s.push_back(static_cast<char>('0' + static_cast<int>(v % 10)));
        v /= 10;
    }
    if (neg) s.push_back('-');
    std::reverse(s.begin(), s.end());
    return s;
}

struct Solver {
    std::vector<System> systems;
    std::vector<int> x;
    i128 answer = 0;

    Solver() : systems(kN + 2) {
        Mat base;
        base.reserve(kN + 1);
        for (int k = 1; k <= kN + 1; ++k) {
            Vec d(kN + 1, 0.0);
            d[kN] = 1.0;
            for (int j = kN - 1; j >= 0; --j) d[j] = -k * d[j + 1];
            if (k % 2 == 1) {
                for (double& v : d) v = -v;
            }
            base.push_back(std::move(d));
        }
        systems[kN + 1] = System{{}, {}, base};
        for (int k = kN; k > kM; --k) {
            systems[k] = eliminate_one(systems[k + 1]);
        }
    }

    void dfs(const int idx) {
        int l = kLower[idx];
        int r = kUpper[idx] - 1;
        if (idx > kM) {
            const auto p = infer_range(systems[idx], x);
            l = std::max(l, p.first);
            r = std::min(r, p.second);
        }
        if (l > r) return;

        if (idx == kN) {
            while (l <= r && !valid_endpoint(x, l)) ++l;
            while (l <= r && !valid_endpoint(x, r)) --r;
            if (l > r) return;

            const i64 count = static_cast<i64>(r - l + 1);
            const i64 linear_sum = static_cast<i64>(count * static_cast<i64>(l + r) / 2);
            i64 prefix_sum = 0;
            for (int v : x) prefix_sum += v;
            answer += static_cast<i128>(linear_sum) + static_cast<i128>(prefix_sum) * count;
            return;
        }

        for (int v = l; v <= r; ++v) {
            x.push_back(v);
            dfs(idx + 1);
            x.pop_back();
        }
    }

    i128 solve() {
        dfs(1);
        return answer;
    }
};

}  // namespace

int main() {
    Solver solver;
    std::cout << to_string_i128(solver.solve()) << '\n';
    return 0;
}

Python

from __future__ import annotations

import re
import shutil
import subprocess
from pathlib import Path

ANSWER_RE = re.compile(r"answer\s*:\s*(.+)$", re.IGNORECASE)
EQUAL_RE = re.compile(r"=\s*(.+)$")


def parse_output(stdout: str) -> str:
    lines = [line.strip() for line in stdout.splitlines() if line.strip()]
    if not lines:
        return ""
    answers = []
    equals = []
    for line in lines:
        m1 = ANSWER_RE.search(line)
        if m1:
            answers.append(m1.group(1).strip())
        m2 = EQUAL_RE.search(line)
        if m2:
            equals.append(m2.group(1).strip())
    if answers:
        return answers[-1]
    if equals:
        return equals[-1]
    return lines[-1]


def should_skip_cpp_checkpoints(src: Path) -> bool:
    try:
        text = src.read_text(encoding="utf-8", errors="ignore")
    except OSError:
        return False
    return "--skip-checkpoints" in text


def run_cpp(binary: Path, src: Path, root: Path) -> str:
    cmd = [str(binary)]
    if should_skip_cpp_checkpoints(src):
        cmd.append("--skip-checkpoints")

    try:
        return subprocess.check_output(cmd, text=True, cwd=root)
    except subprocess.CalledProcessError:
        return subprocess.check_output(cmd, text=True, cwd=src.parent)


def solve() -> str:
    problem_id = __file__.split("Euler")[-1].split(".")[0]
    root = Path(__file__).resolve().parent.parent
    src = root / "solutionsCpp" / f"Euler{problem_id}.cpp"
    binary = root / "solutionsCpp" / f".euler{problem_id}_py_bridge"

    if not binary.exists() or src.stat().st_mtime > binary.stat().st_mtime:
        compiler = shutil.which("clang++") or shutil.which("g++")
        if not compiler:
            raise RuntimeError("No C++ compiler found (clang++/g++).")
        subprocess.check_call([compiler, "-std=c++17", "-O2", str(src), "-o", str(binary)])

    output = run_cpp(binary=binary, src=src, root=root)
    parsed = parse_output(output)
    if not parsed:
        raise RuntimeError(f"Euler{problem_id} bridge produced empty output.")
    return parsed


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

Java

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

public class Euler438 {
    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("Euler438.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(".euler438_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 Euler438 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("Euler438 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("Euler438 C++ bridge produced empty output.");
        }
        return parsed;
    }

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