Problem 44 — Pentagon Numbers

solved Tue, 3 Dec 2019, 23:39 · level 2 · difficulty 6% projecteuler.net github

Pentagonal numbers are generated by the formula, $P_n=n(3n-1)/2$. The first ten pentagonal numbers are: $$1, 5, 12, 22, 35, 51, 70, 92, 117, 145, \dots$$

It can be seen that $P_4 + P_7 = 22 + 70 = 92 = P_8$. However, their difference, $70 - 22 = 48$, is not pentagonal.

Find the pair of pentagonal numbers, $P_j$ and $P_k$, for which their sum and difference are pentagonal and $D = |P_k - P_j|$ is minimised; what is the value of $D$?


Problem Analysis

A pentagonal number is defined by the closed form P(n) = n(3n-1)/2, generating the sequence 1, 5, 12, 22, 35, 51, ... for positive integers n. The problem asks me to find two pentagonal numbers whose sum and difference are both themselves pentagonal, and among all such pairs to minimise the difference D. The crux is being able to answer one question very fast: "is this integer pentagonal?" If that test is constant time, then a straightforward double loop over candidate pairs becomes practical.

The test follows from inverting the defining quadratic. Setting n(3n-1)/2 = m and applying the quadratic formula gives n = (1 + sqrt(1 + 24m)) / 6. The integer m is pentagonal precisely when this expression is a positive whole number. That is a single square root plus a few arithmetic operations - O(1), with no table to build or search. The second decisive observation is about search order. If I fix the larger index i and scan the smaller index j downward, the difference P(i)-P(j) grows monotonically as j shrinks, and the smallest possible difference at outer index i is P(i)-P(i-1), which itself grows with i. So the first valid pair I encounter, with i ascending and j descending, is provably the global minimum. The work is O(i_answer^2) constant-time tests, where i_answer is the outer index at which the answer surfaces - a few thousand in practice.

Solution Approach

Ascending-Outer, Descending-Inner Pair Search with an Algebraic Membership Test

Both the Python (p0044_s0.py) and C (p0044_s0.c) files implement the same algorithm. The outer loop drives i upward from 1 with no fixed bound. For each i I compute p_i once via the closed-form helper, then run an inner loop with j descending from i-1 to 1. For each pair I form the difference p_i - p_j and the sum p_i + p_j and test both with the inverse-formula membership check. The moment both pass, I return the difference.

The descending inner loop is the design choice that justifies the early return. Scanning j from large to small means the first valid pair found for a given i has the smallest difference achievable at that i. Combined with the ascending outer loop and the monotonicity of P(i)-P(i-1) in i, the very first valid pair found anywhere is the global minimum, so there is no reason to keep searching.

The membership test differs subtly between the languages. In Python I compute n = (1 + sqrt(1 + 24*m)) / 6 and call float.is_integer() on it, relying on the float landing exactly on a whole value - acceptable for these magnitudes. In C I take the defensive floating-point route: I compute the same candidate, snap it to the nearest integer with round(), then recompute nth_pentagonal(ni) in exact integer arithmetic and compare against the original m, additionally guarding ni > 0. This round-then-verify pattern is immune to a value reading as 4.9999999999 instead of 5.0.

workbench

Test cases

categoryinputanswer
main 5482660

Results

solutioncategoryanswerverdict avg seconds cavg seconds py
p0044_s0 main 5482660 correct 0.015063 (63) 0.442138 (63)

Files

Tags & blogs