Same Five Digits

April 19, 2011

[Today’s exercise was written by guest author Bob Miller. Bob has been writing system software for Unix since the VAX was new and shiny, and his current hobby is writing Scheme interpreters. Suggestions for exercises are always welcome, or you may wish to contribute your own exercise; feel free to contact me if you are interested.]

Enigma is a weekly column in New Scientist. Every week it has a new puzzle. Some of the Enigma puzzles could be solved using a computer.

I have written down three different 5-digit perfect squares, which between them use five different digits. Each of the five digits is used a different number of times, the five numbers of times being the same as the five digits of the perfect squares. No digit is used its own number of times. If you knew which digit I have used just once you could deduce my three squares with certainty.

What are my three perfect squares?

Your task is to write a program that finds and prints the three perfect squares. When you are finished, you are welcome to read or run a suggested solution, or to post your own solution or discuss the exercise in the comments below.

Let’s look at that slowly. X is a list of all five-digit squares that contain only the digits 1 through 5. The inner x ranges from 100 (because 1002 is the smallest 5-digit number) to 236 (because 2362 is the smallest square greater than 55555). Then the predicate (< 0 (apply max (digits x2)) 6) passes only those numbers with the digits 1 through 5. There are 9 such numbers, the squares of 111, 112, 115, 182, 185, 188, 211, 229, and 235.

On the next line, the three "in" clauses form the cross product of the nine squares, a total of 93=729 triplets. The remaining two clauses on that line eliminate duplicates, reducing the number of triplets from 729 to 84, which by the binomial theorem is the number of ways 3 items can be chosen from a list of 9:

Next we want to calculate the “signature” of the digit counts in the solution. We calculate the fifteen digits in d, and the signature of the counts in s.

Now we return to the puzzle. Each of the five digits is used a different number of times, which is tested by the predicate (= (length (unique = (sort < (map cdr s)))) 5). The counts have to be the same as the digits, which is tested by the predicate (equal? (unique = d) (unique = (sort < (map cdr s)))); the solution works if you omit that predicate, but I couldn't articulate a clear reason why, so I included it. Finally, no digit may be used its own number of times, which is tested by the ok? function:

I was going to invert the s matrix to determine the correct answer, but it is obvious by inspection that there is only one triplet where the digit used once is unique, and I am lazy. The solution to the puzzle is the triplet 1112 = 12321, 1822 = 33124, and 1852 = 34225.

Here’s a ruby version with some of the ideas from above incorporated. In a couple of places I use inject() to figure out if something is true for all elements of a hash. In this case inject() will return an array of two values (a pair) with the key the first element and the value as the second element. In both cases we initialize with “true” and then “and” with our test.

# squares is a hash with the square and another hash that is the digits and their count. For
# example { 12321 => {"1"=>2, "2"=>2, "3"=>1}, 33124 => { "3"=>2, "1"=>1, "2"=>1, "4"=>1}... }
squares = {}
# We're going from 100 (squared to 10000) to 236 (squared to 55696, first number greater than all
# fives. See http://programmingpraxis.com/2011/04/19/same-five-digits/ for the explanation of
# why this is true and why we're only searching for numbers containing the digits 0-5.
100.upto(236) do |n|
h = Hash.new(0)
square = (n**2)
square.to_s.each_char { |d| h[d] += 1 }
squares[square] = h if h.inject(true) { |r, pair| r && ("012345".index(pair[0]) != nil) }
end
# solution_hash will look be an array of the three squares and a hash of digit counts of the three numbers.
# For example { [12321, 12544, 55225] => {"1"=>"3", "2"=>"5", "3"=>"1", "5"=>"4", "4"=>"2"},
# [12321, 33124, 34225] => {"1"=>"3", "2"=>"5", "3"=>"4", "4"=>"2", "5"=>"1"} ... }
solution_hash = {}
# We need to go through the squares hash and generate a new hash, h2 which contains a merge of
# three hashes derived by adding the values together in the three. After we have this
# new hash, we convert the values strings for final processing. Our final processing is a series
# of checks. First make sure that the number of keys is five and that the number of values is five.
# Then make sure that the keys and values contain the same digits, and finally make sure that none
# of the values is equal to its key. If all of these are true then add this solution to the solution
# hash if it's not already there.
squares.each_pair do |k1, v1|
squares.each_pair do |k2, v2|
squares.each_pair do |k3, v3|
next if (k1 == k2) || (k2 == k3) || (k1 == k3)
h2 = v1.merge(v2){|k4, oldval1, newval1| newval1+oldval1}.merge(v3){|k5, oldval2, newval2| newval2+oldval2}
h2.each_pair {|k6, v6| h2[k6] = v6.to_s }
if h2.length == 5 && h2.values.uniq.length == 5 &&
h2.keys.sort.eql?(h2.values.sort) && h2.inject(true) {|r, pair| r && (pair[0] != pair[1]) }
solution_hash[[k1, k2, k3].sort] = h2 if !solution_hash.has_key?([k1, k2, k3].sort)
end
end
end
end
# Display all the solutions.
solution_hash.each_pair do |k, v|
puts "Solution: #{k} #{v}"
end

I got one constraint wrong at first – the one that each of the
five counts be used – and thought the unique single digit was 4.
After correcting that, the following interaction prints the
possible pairs of the single digit and the sequence of the
fifteen digits, from which the three numbers can be read.

I love using Python’s built-in set() object to “uniquify” and also to compare if two sequences have the same stuff irrespective of order. And, as much fun as it to write an combination-generator, here I rely on the combinations() function from Python’s itertools library.

from itertools import combinations
# 100*100 is the smallest 5-digit perfect square (10000)
# 316*316 is the largest 5-digit perfect square (99856)
# we'll be doing alot of string processing on them,
# so convert the square to a string
five_dig_squares = [str(x*x) for x in range(100, 317)]
print("found %d 5-digit perfect squares" % len(five_dig_squares))
candidates_found = 0
# use combinations() from Python's itertools library to cycle
# through the squares in unique three-up sets
for set_of_three in combinations(five_dig_squares, 3):
# string-concatenate the three squares
fifteen_digits = "".join(set_of_three)
# pass the string to set()
# since fifteen_digits is a string, and string
# is a sequence, the set() constructor naturally
# breaks the sequence up, and keeps only unique occurances
unique_digits = set(fifteen_digits)
# if the size of the unique_digits set is not exactly 5,
# then consider the next set_of_three
if len(unique_digits) != 5:
continue
# create a dictionary where the key is the character, and the value is the count
count_digits = dict((c, str(fifteen_digits.count(c))) for c in unique_digits)
# if any of the keys (a given digit) match the values (that digit's number
# of occurrences), then this is not a viable candidate
if sum([1 for k, v in count_digits.items() if k == v]) > 0: continue
# create a set of the count values (the numbers of occurrences)
count_digits_values = set(count_digits.values())
# and if the set digits we found match the set of occurrences, then
# we have a candidate... print out the info
if unique_digits == count_digits_values:
print(set_of_three)
print(unique_digits)
print(count_digits)
print()
candidates_found += 1
print("candidates found: %d" % candidates_found)

I too found seven such numbers (and not one as specified).
Another optimization may be that none of the numbers can contain the zero digit (since no appearing number can appear zero times). That should cut down overall runtime a bit further.