Problem 43 — Sub-string Divisibility

solved Tue, 3 Dec 2019, 12:59 · level 0 · difficulty 3% projecteuler.net github

The number, $1406357289$, is a $0$ to $9$ pandigital number because it is made up of each of the digits $0$ to $9$ in some order, but it also has a rather interesting sub-string divisibility property.

Let $d_1$ be the $1$st digit, $d_2$ be the $2$nd digit, and so on. In this way, we note the following:

  • $d_2d_3d_4=406$ is divisible by $2$
  • $d_3d_4d_5=063$ is divisible by $3$
  • $d_4d_5d_6=635$ is divisible by $5$
  • $d_5d_6d_7=357$ is divisible by $7$
  • $d_6d_7d_8=572$ is divisible by $11$
  • $d_7d_8d_9=728$ is divisible by $13$
  • $d_8d_9d_{10}=289$ is divisible by $17$

Find the sum of all $0$ to $9$ pandigital numbers with this property.


Problem Analysis

A pandigital number over the digits 0 to 9 is a ten-digit arrangement that uses each digit exactly once. There are 10! = 3,628,800 such arrangements, since each is a permutation of the ten digits. The brute-force route would generate every permutation and test each against the seven divisibility rules. That is computationally feasible but profoundly wasteful: nearly every permutation fails the first constraint, so almost all of the work is discarded immediately.

The structural insight that makes this problem genuinely easy is that the seven three-digit windows overlap by two digits. The window d2 d3 d4 shares d3 d4 with d3 d4 d5, which shares d4 d5 with d4 d5 d6, and so on down to d8 d9 d10. Whenever constraints share variables, they can be propagated: once I fix one window, the adjacent window is almost completely determined - only one new digit is free, and it must simultaneously satisfy a divisibility rule and avoid repeating any digit already placed. This reduces the search from an exhaustive scan of permutations to a narrow backtracking walk with aggressive constraint propagation. The worst-case asymptotic bound is still O(10!), but the real cost collapses to a few thousand node visits, completing in microseconds. The first digit d1 carries no divisibility constraint at all, so it is simply whichever digit remains unused once the other nine positions are settled.

Solution Approach

Right-to-left Constrained Backtracking over the Divisor Chain

Both the C file and the Python file implement the same algorithm and differ only in language idiom. The core is a recursive descent over the divisor chain 17 -> 13 -> 11 -> 7 -> 5 -> 3 -> 2, building the number from right to left one digit at a time.

I seed the search with a precomputed constant table of the 44 valid three-digit multiples of 17 that have no repeated digit (such as "017", "289", "986"). These are exactly the legal values for the rightmost window d8 d9 d10. Starting from this table eliminates one full recursion level and makes the entry conditions explicit: I have already discharged the divisibility-by-17 constraint before recursion even begins.

From each seed I call the recursive generator with the current partial string and the next divisor to satisfy. At each step I iterate over the digits '0' through '9', skip any digit already present in the partial string, prepend the candidate, and check whether the leading three characters of the new string are divisible by the current divisor. If they are not, I prune that branch immediately. If they are, I recurse with the next divisor in the chain. When the chain is exhausted (after satisfying 2), the partial string holds positions d2 through d10; I locate the single unused digit, prepend it as d1, convert the ten-character string to an integer, and accumulate it.

I chose to build right to left rather than left to right because the divisibility window always sits at the first three characters of the partial string. Prepending keeps the validation at a fixed offset, so the index arithmetic never changes as the string grows - this removes a whole class of off-by-one errors. The C version encodes the divisor transition in a switch statement (next_divisor) and accumulates into a global total_sum using a fixed-size character buffer and memcpy, so it never touches the heap. The Python version expresses the same transition as a dictionary lookup and uses yield from recursion, so the top-level aggregation consumes results lazily. Functionally they are identical; the C avoids allocation entirely, while the Python reads more declaratively.

workbench

Test cases

categoryinputanswer
main 16695334890

Results

solutioncategoryanswerverdict avg seconds cavg seconds py
p0043_s0 main 16695334890 correct 0.000007 (63) 0.000186 (63)

Files

Tags & blogs