Project Euler Problem 68 Solution

Project Euler Problem 68 Solution

Magic 5-gon Ring

by {BetaProjects} | Project Euler & HackerRank
Difficulty: Hard

Project Euler Problem 68 Statement

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.

Total Solution Set
9 4,2,3; 5,3,1; 6,1,2
9 4,3,2; 6,2,1; 5,1,3
10 2,3,5; 4,5,1; 6,1,3
10 2,5,3; 6,3,1; 4,1,5
11 1,4,6; 3,6,2; 5,2,4
11 1,6,4; 5,4,2; 3,2,6
12 1,5,6; 2,6,4; 3,4,5
12 1,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?


Solution

This script solves a variation of the "magic gon ring" puzzle. We have $2N$ numbers from $1$ to $2N$, which we arrange into an outer ring of $N$ numbers and an inner ring of $N$ numbers. We want each "spoke" to sum to a constant $S$. Mathematically, if the outer numbers are $o_1, o_2, \ldots, o_N$ and the inner numbers are $i_1, i_2, \ldots, i_N$, we require:

\[ o_k + i_k + i_{k+1} = S \quad \text{for each } k, \]

where $i_{N+1}$ wraps around to $i_1$. The script first narrows down which sets of numbers can form the outer ring by using a derived sum condition, effectively pruning the search space.

It then fixes the smallest number in the outer ring to avoid symmetrical duplicates. Next, it pairs this smallest number with two inner numbers that satisfy the sum requirement $o + i + j = S$. After establishing these initial links, a recursive function constructs the rest of the configuration, ensuring each outer number pairs with two consecutive inner numbers to meet the magic sum condition.

Whenever a complete valid arrangement is found (i.e., all numbers are assigned and each spoke sums to $S$), the script formats the result as a string and includes it in a list of solutions. Finally, those solutions are sorted and returned, ensuring all possible valid "magic gon" arrangements are revealed.

I'm trying something new by including all the "how-to" in comments inside the code listed below.

HackerRank version

HackerRank Project Euler 68: Given $N$, $3\le N\le 10$, which represents the $N-gon$ and the total $S$ print all concatenated solution strings in alphabetical sorted order.

Python Source Code

import itertools

def solve(N, S):
    """
    Solve a variation of the 'magic gon ring' puzzle:
    We have 2N numbers (1 to 2N) arranged such that:
      - N numbers form the 'outer ring' (outer nodes).
      - N numbers form the 'inner ring' (inner nodes).
    Each 'spoke' consists of one outer node and two adjacent inner nodes
    and must sum to S. For example, for k = 1 to N:

        outer[k] + inner[k] + inner[k+1] == S

    with inner[N+1] wrapping around to inner[1].
    
    Parameters:
        N (int): the number of 'spokes' or the size of each ring.
        S (int): the target sum for each spoke.

    Returns:
        list: A sorted list of valid string representations for all solutions.
    """

    def get_arrangement(outer, inner):
        """
        Given the final outer and inner rings, produce a single string
        representation by concatenating each spoke (outer + two inner).
        Example:
            outer = [o1, o2, ..., oN]
            inner = [i1, i2, ..., iN]
        Then each segment is (o_k, i_k, i_{k+1}), wrapped around for k=N.
        """
        return ''.join(f"{o}{i1}{i2}" for o, i1, i2 in 
                       zip(outer, inner, inner[1:] + inner[:1]))
    
    # All numbers from 1 up to 2N
    nums = range(1, 2*N + 1)

    # A derived sum condition for the outer ring to reduce the search space.
    # This formula (N * (4N + 2 - S)) comes from algebraic manipulation of the
    # sum constraints, ensuring we only choose outer sets that could potentially
    # form valid solutions.
    target_outer_sum = N * (4*N + 2 - S)

    # This list will store all valid solutions (string representations).
    solutions = []
    
    # 1) Find valid combinations of numbers for the outer ring.
    for outer in itertools.combinations(nums, N):
        # Prune quickly if the sum of the chosen 'outer' doesn't match the
        # required sum condition.
        if sum(outer) != target_outer_sum:
            continue
            
        # The remaining numbers become candidates for the inner ring.
        inner_nums = set(nums) - set(outer)
        
        # Common trick in 'magic gon' puzzles: pick the smallest
        # outer number to avoid duplicates / symmetrical solutions.
        first = min(outer)
        remaining_outer = set(outer) - {first}
        
        # 2) Try each possible 'second' number from the inner candidates,
        # and compute a 'third' to complete the first spoke (first + second + third = S).
        for second in inner_nums:
            third = S - first - second
            # If 'third' is not a valid distinct inner number, skip.
            if third not in inner_nums or third == second:
                continue
                
            # 3) Build the rest of the solution recursively.
            def build(curr_outer, curr_inner, avail_outer, avail_inner):
                """
                Recursive function that tries to assign numbers for the
                outer and inner rings so that each spoke sums to S.
                
                Parameters:
                  curr_outer (list): partial assignment of outer numbers so far
                  curr_inner (list): partial assignment of inner numbers so far
                  avail_outer (set): available (unused) outer numbers
                  avail_inner (set): available (unused) inner numbers
                """
                # Base case: if we have used up all inner numbers, check the final spoke.
                if not avail_inner:
                    # We should have exactly one outer number left to complete the ring.
                    # Verify that every spoke (including the final wrap) sums to S.
                    if all(
                        o + i1 + i2 == S 
                        for o, i1, i2 in zip(
                            curr_outer + [list(avail_outer)[0]],
                            curr_inner,
                            curr_inner[1:] + curr_inner[:1]
                        )
                    ):
                        # If valid, transform this arrangement into a string and record it.
                        solutions.append(get_arrangement(
                            curr_outer + [list(avail_outer)[0]],
                            curr_inner
                        ))
                    return
                
                # Otherwise, keep assigning available outer numbers and the corresponding inner number.
                for o in avail_outer:
                    # The needed inner number is the one that completes the sum with
                    # the last assigned inner node and the current outer node.
                    i = S - o - curr_inner[-1]
                    if i in avail_inner:
                        # Recurse with these new assignments removed from availability.
                        build(
                            curr_outer + [o],
                            curr_inner + [i],
                            avail_outer - {o},
                            avail_inner - {i}
                        )
            
            # Initialize the recursion with:
            #   - 'first' as the initial outer
            #   - 'second' and 'third' as the first two inner numbers
            #   - 'remaining_outer' and updated 'inner_nums' 
            build(
                [first],             # curr_outer
                [second, third],     # curr_inner
                remaining_outer,     # avail_outer
                inner_nums - {second, third}  # avail_inner
            )
    
    # Return all sorted solutions (string representations).
    return sorted(solutions)

N, S = map(int, input().split())
print(*solve(N, S), sep='\n')  # Print each valid solution on its own line