Problem 68 — Magic 5-gon Ring

solved Tue, 15 Jul 2025, 12:32 · level 3 · difficulty 9% projecteuler.net github

Consider the following "magic" 3-gon ring, filled with the numbers 1 to 6, and each line adding to nine.


Working clockwise, and starting from the group of three with the numerically lowest external node (4,3,2 in this example), each solution can be described uniquely. For example, the above solution can be described by the set: 4,3,2; 6,2,1; 5,1,3.

It is possible to complete the ring with four different totals: 9, 10, 11, and 12. There are eight solutions in total.

TotalSolution Set
94,2,3; 5,3,1; 6,1,2
94,3,2; 6,2,1; 5,1,3
102,3,5; 4,5,1; 6,1,3
102,5,3; 6,3,1; 4,1,5
111,4,6; 3,6,2; 5,2,4
111,6,4; 5,4,2; 3,2,6
121,5,6; 2,6,4; 3,4,5
121,6,5; 3,5,4; 2,4,6

By concatenating each group it is possible to form 9-digit strings; the maximum string for a 3-gon ring is 432621513.

Using the numbers 1 to 10, and depending on arrangements, it is possible to form 16- and 17-digit strings. What is the maximum 16-digit string for a "magic" 5-gon ring?



Problem Analysis

A "magic" n-gon ring is a small graph built from two layers of nodes. The inner layer is a cycle of n nodes, each connected to its two neighbours; the outer layer attaches one external node to each inner node. A "line" of the ring is a triple - an outer node, the inner node it hangs off, and the next inner node clockwise. I fill the 2n positions with the integers 1 to 2n and require that all n lines share a common sum S, the magic total. For n = 5 the goal is the lexicographically largest 16-digit string formed by concatenating the lines, starting from the line whose outer node is smallest (a canonical start that removes rotational ambiguity).

The first key observation is a counting identity. Each outer node belongs to exactly one line, while each inner node is shared between two lines (it is the second node of one line and the first node of the next). Summing all n lines gives n*S = (sum of outer nodes) + 2*(sum of inner nodes). Since 1 + 2 + ... + 2n is fixed, choosing which n numbers form the inner ring already fixes S. The second observation is more powerful still: once the inner ring permutation is fixed and I pin the smallest outer node at the first position, every other outer value is forced by outer[i] = S - inner[i] - inner[i+1]. I never search the outer positions at all - I compute them. This collapses an exponential search over all 2n positions into a permutation enumeration over only the n inner positions, with a cheap O(n) check per candidate. A final structural insight specific to the 16-digit requirement: 10 is the only two-digit value in 1..10, and an inner node contributes to two lines (so a 10 inside would add two digits twice), whereas an outer node contributes once. To get exactly 16 digits rather than 17, 10 must sit on an outer node, so the inner ring is drawn only from {1, ..., 9}. The resulting complexity is O(P(min(9, 2n), n) * n), which for n = 5 is at most P(9, 5) = 15120 candidates - trivially fast.

Solution Approach

Inner-ring Enumeration with Forced Outer Values

Both files implement the same algorithm. The Python version is iterative, driven by itertools.permutations; the C version is a recursive backtracking search. The four logical steps are identical.

Step 1 - Enumerate inner ring permutations. I generate all ordered arrangements of n values chosen from {1, ..., min(9, 2n)}. The cap of 9 encodes the "10 belongs on the outside" constraint that yields a 16-digit result for n = 5; using min(9, 2n) keeps the code correct for the smaller dev cases (n = 3, 4), where the cap is irrelevant, and for the larger extra cases. In C, solve_recursive fills inner_choice[depth] with each unused value, marks it in the used direct-address array, recurses, and unmarks on the way back out.

Step 2 - Compute and validate adjacent inner sums. For a candidate inner ring I compute the n sums s[k] = inner[k] + inner[(k+1) mod n]. These are the inner contribution of each line. If any two sums coincide, the corresponding outer values would collide - two lines would demand the same outer node - so I reject the candidate immediately. Python uses len(set(inner_sums)) != n; C uses a nested loop comparison. Rejecting early here keeps the inner loop tight.

Step 3 - Force the outer values. I place the smallest still-available number at outer position 0, canonicalising the clockwise start exactly as the problem requires. The magic total becomes S = outer[0] + s[0]. Every remaining outer value is then forced: outer[i] = S - s[i]. I check each forced value is in range and still available; the first failure aborts the candidate. This is the heart of the algorithm - the outer ring is computed, never searched.

Step 4 - Build and compare the string. A valid arrangement is concatenated line by line into a digit string and compared with the running best. Python converts the string to an arbitrary-precision int and compares numerically. C compares with a custom cmp_magic that ranks by length first, then lexicographically - equivalent to numeric comparison for equal-length non-negative integers, and free of overflow concerns.

workbench

Test cases

categoryinputanswer
dev result_length = 9 ring_size = 3 432621513
dev result_length = 12 ring_size = 4 462723831516
main result_length = 16 ring_size = 5 6531031914842725
extra result_length = 21 ring_size = 6 692122310348451151719
extra result_length = 26 ring_size = 7 87494612611315115314321027
extra result_length = 31 ring_size = 8 5981381151614621624114712731039
extra result_length = 36 ring_size = 9 109518511716166215271473133812841149

Results

solutioncategoryanswerverdict avg seconds cavg seconds py
p0068_s0 dev 432621513 correct 0.000007 (63) 0.000198 (63)
p0068_s0 dev 462723831516 correct 0.000067 (63) 0.002733 (63)
p0068_s0 main 6531031914842725 correct 0.000676 (63) 0.027919 (63)
p0068_s0 extra 692122310348451151719 correct 0.002432 (63) 0.082908 (63)
p0068_s0 extra 87494612611315115314321027 correct 0.007205 (63) 0.234262 (63)
p0068_s0 extra 5981381151614621624114712731039 correct 0.015768 (63) 0.407487 (63)
p0068_s0 extra 109518511716166215271473133812841149 correct 0.019294 (63) 0.412211 (63)

Files

Tags & blogs