From 155dd334dadcd06e712b653474f2737b4e4a1f37 Mon Sep 17 00:00:00 2001 From: Mark Oveson Date: Mon, 31 Oct 2016 13:09:00 -0600 Subject: [PATCH] Fast alphametics solver that uses partials to limit the set of viable permutations. --- exercises/alphametics/example.rb | 163 +++++++++++++++++++------------ 1 file changed, 100 insertions(+), 63 deletions(-) diff --git a/exercises/alphametics/example.rb b/exercises/alphametics/example.rb index 2a20e14b26..49446d4326 100644 --- a/exercises/alphametics/example.rb +++ b/exercises/alphametics/example.rb @@ -1,98 +1,135 @@ module BookKeeping - VERSION = 3 + VERSION = 4 end class Alphametics - def solve(puzzle) - letters = Hash.new(0) - puzzle.scan(/[a-zA-Z]/) { |w| letters[w] += 1 } - possible_values(letters.keys) do |letters_values| - return letters_values if valid?(puzzle, letters_values) - end + def self.solve(equation) + new.solve(equation) + end - nil + def solve(equation) + @prime_solver = AlphaSolver.new(equation) + solve_using_partials end private - def possible_values(letters) - (0..9).to_a.combination(letters.length) do |combined_integers| - combined_integers.permutation do |permutated_integers| - yield permutated_integers.map.with_index { |integer, index| - [letters[index], integer] - }.to_h - end + attr_accessor :prime_solver + + def solve_using_partials + prime_solver.partial_solutions.each do |partial_solution| + sub_solver = AlphaSolver.new(prime_solver.partial_equation(partial_solution)) + sub_solution = sub_solver.first_solution + return sub_solution.merge(partial_solution) if sub_solution end + {} end - def valid?(puzzle, letters_values) - equation = puzzle.gsub(/[a-zA-Z]/, letters_values) - Equation.new(equation).valid? - end end -class Equation - attr_reader :equation +class AlphaSolver - def initialize(equation) - @equation = equation + def initialize(input_equation) + @input_equation = input_equation.gsub('^', '**') + @puzzle = Puzzle.new(input_equation) end - def valid? - return false if has_leading_zeros? + def partial_solutions + AlphaSolver.new(puzzle.simplified).all_solutions + end - expression, result = equation.split('==') + def all_solutions + numeric_permutations.map { |values| result_table if solution?(values) }.compact + end - numbers = [] - operators = [] + def first_solution + numeric_permutations.each { |values| return result_table if solution?(values) } + nil + end - expression.scan(/\d+|\+|\-|\*|\/|\^/).each do |token| - case token - when /^\d+$/ - numbers.push(token.to_i) - when '+', '-', '*', '/', '^' - calculate_last(numbers, operators) if has_precedence?(operators, token) - operators.push(token) - end - end + def partial_equation(partial_solution) + input_equation.tr(partial_solution.keys.join, partial_solution.values.join) + end - until operators.empty? - calculate_last(numbers, operators) - end + private + + attr_reader :input_equation, :puzzle + attr_accessor :proposed_values - numbers.last == result.to_i + def solution?(values) + self.proposed_values = values.join + proposed_equation_qualified? && proposed_equation_evaluates? end - private + def proposed_equation + input_equation.tr(puzzle_letters, proposed_values) + end - def has_leading_zeros? - equation.match(/^0\d+|\D0\d+/) + def numeric_permutations + puzzle.numeric_permutations end - def has_precedence?(operators, token) - return false if operators.empty? - prev_operator = operators.last + def puzzle_letters + puzzle.letters + end - case token - when '+', '-' - prev_operator == '*' || prev_operator == '/' || prev_operator == '^' - when '*', '/' - prev_operator == '^' - else - false - end + def proposed_equation_qualified? + (proposed_equation =~ /\b0\d+/).nil? end - def calculate_last(numbers, operators) - right = numbers.pop - left = numbers.pop - operator = as_ruby_operator(operators.pop) - result = left.send(operator, right) - numbers.push(result) + def proposed_equation_evaluates? + eval(proposed_equation) end - def as_ruby_operator(operator) - operator == '^' ? '**' : operator + def result_table + Hash[puzzle_letters.chars.zip(result_numbers)] end + + def result_numbers + proposed_values.chars.map(&:to_i) + end + end + +class Puzzle + + PATTERNS = {mod_10: ' % 10', + adjacent_letters: /(\b)([A-Z]{1,})([A-Z])/, + equation_left_side: /(.*)( == )/} + + def initialize(string_equation) + @string_equation = string_equation + end + + def letters + @letters ||= string_equation.scan(/[A-Z]/).uniq.join + end + + def numeric_permutations + @numeric_permutations ||= unused_numbers.to_a.permutation(letter_count) + end + + def simplified + @simplified ||= string_equation + .gsub(PATTERNS[:adjacent_letters], "\\1\\3") + .gsub(PATTERNS[:equation_left_side], "(\\1)#{PATTERNS[:mod_10]}\\2") + end + + private + + attr_reader :string_equation + + def letter_count + @letter_count ||= letters.length + end + + def unused_numbers + @unused_numbers ||= (0..9).to_a.map(&:to_s) - used_numbers + end + + def used_numbers + @used_numbers ||= string_equation.gsub(PATTERNS[:mod_10], '').scan(/\d/).uniq + end + +end \ No newline at end of file