Problem 27: Quadratic Primes

View on Project Euler

Project Euler Problem 27 Solution

EulerSolve provides an optimized solution for Project Euler Problem 27, Quadratic Primes, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary We study the quadratic family \(q_{a,b}(n)=n^2+an+b\) under the bounds \(|a| \lt 1000\) and \(|b| \le 1000\). For each choice of coefficients, we start at \(n=0\) and ask how long the values stay prime without interruption. The task is to find the pair \((a,b)\) with the longest initial prime streak and return the product \(ab\). Inside these bounds, the best pair is \(a=-61\) and \(b=971\). It produces 71 consecutive primes for \(n=0,1,\dots,70\), so the required product is \(-59231\). Mathematical Approach Let \(\mathbb{P}\) denote the set of positive primes, and let \(L(a,b)\) be the length of the initial prime run generated by \(q_{a,b}(n)\): $$L(a,b)=\min\{n\ge 0 : q_{a,b}(n)\notin \mathbb{P}\}.$$ If the values at \(n=0,1,\dots,k-1\) are all prime, then \(L(a,b)=k\). The problem is therefore a finite maximization problem over all allowed coefficient pairs. The first value fixes \(b\) At \(n=0\), the polynomial is simply $$q_{a,b}(0)=b.$$ So \(b\) itself must be prime. Because the primality test used by the implementations rejects every integer below 2, this means \(b\) must in fact be a positive prime. Mathematically, that already removes the overwhelming majority of the nominal \(|b|\le 1000\) search interval....

Detailed mathematical approach

Problem Summary

We study the quadratic family \(q_{a,b}(n)=n^2+an+b\) under the bounds \(|a| \lt 1000\) and \(|b| \le 1000\). For each choice of coefficients, we start at \(n=0\) and ask how long the values stay prime without interruption. The task is to find the pair \((a,b)\) with the longest initial prime streak and return the product \(ab\).

Inside these bounds, the best pair is \(a=-61\) and \(b=971\). It produces 71 consecutive primes for \(n=0,1,\dots,70\), so the required product is \(-59231\).

Mathematical Approach

Let \(\mathbb{P}\) denote the set of positive primes, and let \(L(a,b)\) be the length of the initial prime run generated by \(q_{a,b}(n)\):

$$L(a,b)=\min\{n\ge 0 : q_{a,b}(n)\notin \mathbb{P}\}.$$

If the values at \(n=0,1,\dots,k-1\) are all prime, then \(L(a,b)=k\). The problem is therefore a finite maximization problem over all allowed coefficient pairs.

The first value fixes \(b\)

At \(n=0\), the polynomial is simply

$$q_{a,b}(0)=b.$$

So \(b\) itself must be prime. Because the primality test used by the implementations rejects every integer below 2, this means \(b\) must in fact be a positive prime. Mathematically, that already removes the overwhelming majority of the nominal \(|b|\le 1000\) search interval.

The second value constrains the parity of \(a\)

The next term is

$$q_{a,b}(1)=1+a+b.$$

If \(b\) is an odd prime, then \(1+b\) is even, so \(q_{a,b}(1)\) has the same parity as \(a\). To remain prime and exceed 2, this value should usually be odd, which strongly suggests that successful choices of \(a\) are odd once \(b>2\). The implementations do not exploit this observation, but it explains part of the structure of the winning search region.

The value at \(n=b\) gives a structural bound

Substituting \(n=b\) reveals a useful factorization:

$$q_{a,b}(b)=b^2+ab+b=b(b+a+1).$$

Since \(b\) is prime, this term is automatically divisible by \(b\). Therefore a prime run will normally break no later than \(n=b\), because \(q_{a,b}(b)\) is then forced to be composite. The only way around that divisibility trap is the exceptional case

$$b+a+1=1 \quad\Longrightarrow\quad a=-b,$$

which makes \(q_{a,b}(b)=b\) prime again. This identity is not used as a pruning rule in the code, but it is a genuine problem-specific invariant and shows why long runs are rare.

Why a direct search is enough

The coefficient box is small, and most pairs fail quickly. For each admissible pair, one evaluates

$$q_{a,b}(0),\ q_{a,b}(1),\ q_{a,b}(2),\ \dots$$

until the first non-prime value appears. All three implementations use the same simple primality test: reject \(n<2\), handle the even case separately, then try odd divisors up to \(\sqrt{n}\). No sieve, recurrence, or deeper number theory is required for this problem size.

Worked example: the winning quadratic

The maximizing polynomial is

$$q(n)=n^2-61n+971.$$

Its first values are

$$q(0)=971,\qquad q(1)=911,\qquad q(2)=853,$$

and the prime streak continues all the way to \(n=70\). The last prime in that initial block is

$$q(70)=70^2-61\cdot 70+971=1601,$$

while the next value is

$$q(71)=71^2-61\cdot 71+971=1681=41^2.$$

So this pair produces exactly 71 consecutive primes, and no admissible pair produces more.

How the Code Works

Enumerating coefficient pairs

The C++, Python, and Java implementations scan every integer \(a\) from \(-999\) to \(999\) and every integer \(b\) from \(-999\) to \(999\). This is effectively the Project Euler search box, because the only omitted endpoints are \(\pm 1000\), and neither is prime. Before any streak is counted, the implementation checks whether \(b\) itself is prime and skips the pair immediately if it is not.

Counting a prime streak

For each surviving pair, the implementation starts with \(n=0\), computes \(n^2+an+b\), and stops as soon as the result fails the prime test. Negative values, 0, and 1 therefore end the loop at once. The number of successful iterations is exactly the mathematical quantity \(L(a,b)\).

Keeping the best pair and validating the search

Whenever a newly tested pair yields a longer streak than the current record, the stored best coefficients are updated. Before the full search, the implementations also support self-checks using the classical examples \(n^2+n+41\) and \(n^2-79n+1601\), verifying streak lengths 40 and 80. The second example lies outside the final coefficient bounds, but it is still a useful correctness test for the counting routine.

Complexity Analysis

Let \(A=999\) and \(B=999\). The raw scan touches \((2A+1)(2B+1)=1999^2\) coefficient pairs, but only those with prime \(b\) require a full streak count. Since there are 168 positive primes at most 999, the more expensive part is closer to

$$1999\times 168 \approx 3.36\times 10^5$$

candidate quadratics.

If a given pair survives for \(K\) terms and the largest tested value has size about \(M\), then trial division costs \(O(K\sqrt{M})\) for that pair. In this problem \(K\) never exceeds 71 inside the search box, so the runtime is easily manageable. Memory usage is \(O(1)\): the implementations keep only loop counters, the current polynomial value, and the best pair found so far.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=27
  2. Prime number: Wikipedia - Prime number
  3. Trial division: Wikipedia - Trial division
  4. Quadratic polynomial: Wikipedia - Quadratic polynomial
  5. Prime-generating formulas: Wikipedia - Formula for primes

Problem 27 source code

C++

#include <cstdint>
#include <iostream>
#include <string>

namespace {

using i64 = std::int64_t;

struct Options {
    int abs_a = 999;
    int abs_b = 999;
    bool run_checkpoints = true;
};

bool parse_int_after_prefix(const std::string& arg, const std::string& prefix, int& value) {
    if (arg.rfind(prefix, 0U) != 0U) {
        return false;
    }
    const std::string tail = arg.substr(prefix.size());
    if (tail.empty()) {
        return false;
    }

    int parsed = 0;
    for (const char c : tail) {
        if (c < '0' || c > '9') {
            return false;
        }
        parsed = parsed * 10 + static_cast<int>(c - '0');
    }
    value = parsed;
    return true;
}

bool parse_arguments(int argc, char** argv, Options& options) {
    for (int i = 1; i < argc; ++i) {
        const std::string arg(argv[i]);

        if (arg == "--skip-checkpoints") {
            options.run_checkpoints = false;
            continue;
        }
        if (parse_int_after_prefix(arg, "--abs-a=", options.abs_a)) {
            continue;
        }
        if (parse_int_after_prefix(arg, "--abs-b=", options.abs_b)) {
            continue;
        }

        std::cerr << "Unknown argument: " << arg << '\n';
        return false;
    }

    return options.abs_a >= 0 && options.abs_b >= 0;
}

bool is_prime(const i64 n) {
    if (n < 2) {
        return false;
    }
    if ((n & 1LL) == 0LL) {
        return n == 2;
    }
    for (i64 p = 3; p <= n / p; p += 2) {
        if (n % p == 0LL) {
            return false;
        }
    }
    return true;
}

int consecutive_prime_count(const int a, const int b) {
    int n = 0;
    while (true) {
        const i64 value = static_cast<i64>(n) * static_cast<i64>(n) +
                          static_cast<i64>(a) * static_cast<i64>(n) +
                          static_cast<i64>(b);
        if (!is_prime(value)) {
            break;
        }
        ++n;
    }
    return n;
}

int solve(const int abs_a, const int abs_b) {
    int best_a = 0;
    int best_b = 0;
    int best_count = -1;

    for (int a = -abs_a; a <= abs_a; ++a) {
        for (int b = -abs_b; b <= abs_b; ++b) {
            if (!is_prime(b)) {
                continue;
            }
            const int count = consecutive_prime_count(a, b);
            if (count > best_count) {
                best_count = count;
                best_a = a;
                best_b = b;
            }
        }
    }

    return best_a * best_b;
}

bool run_checkpoints() {
    if (consecutive_prime_count(1, 41) != 40) {
        std::cerr << "Checkpoint failed for n^2+n+41" << '\n';
        return false;
    }
    if (consecutive_prime_count(-79, 1601) != 80) {
        std::cerr << "Checkpoint failed for n^2-79n+1601" << '\n';
        return false;
    }
    return true;
}

}  // namespace

int main(int argc, char** argv) {
    Options options;
    if (!parse_arguments(argc, argv, options)) {
        return 1;
    }

    if (options.run_checkpoints && !run_checkpoints()) {
        return 2;
    }

    std::cout << solve(options.abs_a, options.abs_b) << '\n';
    return 0;
}

Python

import sys

def is_prime(n):
    if n < 2:
        return False
    if n % 2 == 0:
        return n == 2
    p = 3
    while p * p <= n:
        if n % p == 0:
            return False
        p += 2
    return True

def consecutive_prime_count(a, b):
    n = 0
    while True:
        value = n * n + a * n + b
        if not is_prime(value):
            break
        n += 1
    return n

def solve(abs_a, abs_b):
    best_a = 0
    best_b = 0
    best_count = -1

    for a in range(-abs_a, abs_a + 1):
        for b in range(-abs_b, abs_b + 1):
            if not is_prime(b):
                continue
            count = consecutive_prime_count(a, b)
            if count > best_count:
                best_count = count
                best_a = a
                best_b = b

    return best_a * best_b

def run_checkpoints():
    if consecutive_prime_count(1, 41) != 40:
        sys.stderr.write("Checkpoint failed for n^2+n+41\n")
        return False
    if consecutive_prime_count(-79, 1601) != 80:
        sys.stderr.write("Checkpoint failed for n^2-79n+1601\n")
        return False
    return True

def parse_int_after_prefix(arg, prefix):
    if not arg.startswith(prefix):
        return False, None
    tail = arg[len(prefix):]
    if not tail:
        return False, None
    try:
        value = int(tail)
        if value < 0:
            return False, None
        return True, value
    except ValueError:
        return False, None

def main():
    abs_a = 999
    abs_b = 999
    run_checkpoints_flag = True
    
    args = sys.argv[1:]
    for arg in args:
        if arg == "--skip-checkpoints":
            run_checkpoints_flag = False
        else:
            success, value = parse_int_after_prefix(arg, "--abs-a=")
            if success:
                abs_a = value
                continue
            success, value = parse_int_after_prefix(arg, "--abs-b=")
            if success:
                abs_b = value
                continue
            sys.stderr.write(f"Unknown argument: {arg}\n")
            return 1
    
    if abs_a < 0 or abs_b < 0:
        sys.stderr.write("Invalid arguments\n")
        return 1
    
    if run_checkpoints_flag and not run_checkpoints():
        return 2
    
    print(solve(abs_a, abs_b))

if __name__ == "__main__":
    main()

Java

class Euler27 {

    private static class Options {
        int abs_a = 999;
        int abs_b = 999;
        boolean run_checkpoints = true;
    }

    private static boolean parse_int_after_prefix(String arg, String prefix, java.util.function.IntConsumer setter) {
        if (!arg.startsWith(prefix)) {
            return false;
        }
        String tail = arg.substring(prefix.length());
        if (tail.isEmpty()) {
            return false;
        }

        int parsed = 0;
        for (int i = 0; i < tail.length(); i++) {
            char c = tail.charAt(i);
            if (c < '0' || c > '9') {
                return false;
            }
            parsed = parsed * 10 + (c - '0');
        }
        setter.accept(parsed);
        return true;
    }

    private static boolean parse_arguments(String[] args, Options options) {
        for (String arg : args) {
            if ("--skip-checkpoints".equals(arg)) {
                options.run_checkpoints = false;
                continue;
            }
            if (parse_int_after_prefix(arg, "--abs-a=", val -> options.abs_a = val)) {
                continue;
            }
            if (parse_int_after_prefix(arg, "--abs-b=", val -> options.abs_b = val)) {
                continue;
            }

            System.err.println("Unknown argument: " + arg);
            return false;
        }

        return options.abs_a >= 0 && options.abs_b >= 0;
    }

    private static boolean is_prime(long n) {
        if (n < 2) {
            return false;
        }
        if ((n & 1L) == 0L) {
            return n == 2;
        }
        for (long p = 3; p <= n / p; p += 2) {
            if (n % p == 0) {
                return false;
            }
        }
        return true;
    }

    private static int consecutive_prime_count(int a, int b) {
        int n = 0;
        while (true) {
            long value = (long)n * n + (long)a * n + b;
            if (!is_prime(value)) {
                break;
            }
            ++n;
        }
        return n;
    }

    private static int solve(int abs_a, int abs_b) {
        int best_a = 0;
        int best_b = 0;
        int best_count = -1;

        for (int a = -abs_a; a <= abs_a; ++a) {
            for (int b = -abs_b; b <= abs_b; ++b) {
                if (!is_prime(b)) {
                    continue;
                }
                int count = consecutive_prime_count(a, b);
                if (count > best_count) {
                    best_count = count;
                    best_a = a;
                    best_b = b;
                }
            }
        }

        return best_a * best_b;
    }

    private static boolean run_checkpoints() {
        if (consecutive_prime_count(1, 41) != 40) {
            System.err.println("Checkpoint failed for n^2+n+41");
            return false;
        }
        if (consecutive_prime_count(-79, 1601) != 80) {
            System.err.println("Checkpoint failed for n^2-79n+1601");
            return false;
        }
        return true;
    }

    public static void main(String[] args) {
        Options options = new Options();
        if (!parse_arguments(args, options)) {
            System.exit(1);
        }

        if (options.run_checkpoints && !run_checkpoints()) {
            System.exit(2);
        }

        System.out.println(solve(options.abs_a, options.abs_b));
    }
}