There are probably more subtle ways to solve this problem like using a constructive solution starting from the end. Anyway, let's try to improve your bruteforce implementation.
Already defined values
permutations('1406357289')
could be written permutations(string.digits)
Conversions in all directions
From a permutation (iterable), you build a string (with join
) then an int which is then converted to a string (in check_divisibility
) to get the chunks of 3 digits which are converted to int.
Also, some of these operations are performed twice...
From a permutation perm
, you can directly compute the value of the chunk at index i
with int(''.join(p[i:i + 3]))
(which involves many conversions but it is hard to do less).
Avoid range(1, 8)
The hardcoded range can be hard to understand and easy to get wrong when other parts of the code get updated because of the magic numbers.
In general, it is best to avoid using range
when what you want is to iterate over an iterable - see also Ned Batchelder's excellent talk: "Loop like a native".
Here, you could write something like (not tested):
def check_divisibility(number):
"""returns True if pandigital number obeys to the divisibility rules."""
divisors = [2, 3, 5, 7, 11, 13, 17]
to_str = str(number)
for idx, div in enumerate(divisors):
if int(to_str[idx + 1: idx + 3 + 1]) % div:
return False
return True
Builtins
This looks like the typical situation where you can use the builtins all
/any
.
def check_divisibility(number):
"""returns True if pandigital number obeys to the divisibility rules."""
divisors = [2, 3, 5, 7, 11, 13, 17]
to_str = str(number)
return all(int(to_str[idx + 1: idx + 3 + 1]) % div == 0
for idx, div in enumerate(divisors))
Now that we remember that we could provide number as a string directly to the function, we'd have something like:
def check_divisibility(number):
"""returns True if pandigital number obeys to the divisibility rules."""
divisors = [2, 3, 5, 7, 11, 13, 17]
return all(int(number[idx + 1: idx + 3 + 1]) % div == 0
for idx, div in enumerate(divisors))
def check_zero_nine_pandigits():
"""generates all 10 pandigital numbers that obey the divisibility rules."""
return (int(''.join(perm)) for perm in permutations(string.digits) if perm[0] != '0' and check_divisibility(''.join(perm)))
And then, we could ask ourselves if we really need that extra function:
def check_zero_nine_pandigits():
"""generates all 10 pandigital numbers that obey the divisibility rules."""
divisors = [2, 3, 5, 7, 11, 13, 17]
return (int(''.join(perm)) for perm in permutations(string.digits)
if perm[0] != '0' and
all(int(''.join(perm)[idx + 1: idx + 3 + 1]) % div == 0 for idx, div in enumerate(divisors)))
Small optimisation
The divisibility criteria with 17 is more restrictive than the one with 2. We could start with that one.
For instance:
def check_zero_nine_pandigits():
"""generates all 10 pandigital numbers that obey the divisibility rules."""
divisors = [(17, 7), (13, 6), (11, 5), (7, 4), (5, 3), (3, 2), (2, 1)]
return (int(''.join(perm)) for perm in permutations(string.digits)
if perm[0] != '0' and
all(int(''.join(perm)[idx: idx + 3]) % div == 0 for div, idx in divisors))
Joining less elements
''.join(perm)[idx: idx + 3]
can be written ''.join(perm[idx: idx + 3])
Final code:
def check_zero_nine_pandigits():
"""generates all 10 pandigital numbers that obey the divisibility rules."""
divisors = [(17, 7), (13, 6), (11, 5), (7, 4), (5, 3), (3, 2), (2, 1)]
return (int(''.join(perm)) for perm in permutations(string.digits)
if perm[0] != '0' and
all(int(''.join(perm[idx: idx + 3])) % div == 0 for div, idx in divisors))