# # Quasigroup Completion in Elixir. # # See # Carla P. Gomes and David Shmoys: # "Completing Quasigroups or Latin Squares: Structured Graph Coloring Problem" # # See also # Ivars Peterson "Completing Latin Squares" # http://www.maa.org/mathland/mathtrek_5_8_00.html # """ # Using only the numbers 1, 2, 3, and 4, arrange four sets of these # numbers into a four-by-four array so that no column or row contains # the same two numbers. The result is known as a Latin square. # ... # The so-called quasigroup completion problem concerns a table that is # correctly but only partially filled in. The question is whether the # remaining blanks in the table can be filled in to obtain a complete # Latin square (or a proper quasigroup multiplication table). # """ # # # This program was created by Hakan Kjellerstrand, hakank@gmail.com # See also my Elixir page: http://www.hakank.org/elxir/ # ## Boris Okner: modified to sync with the latest API, ## change then naming and the result handling. ## defmodule CPSolver.Examples.Hakank.QuasigroupCompletion do import Hakank.CPUtils alias CPSolver.IntVariable # alias CPSolver.Constraint.Sum # alias CPSolver.Constraint.Equal # alias CPSolver.Constraint.AllDifferent.FWC, as: AllDifferent alias CPSolver.Model # # Example from Ruben Martins and Inès Lynce # Breaking Local Symmetries in Quasigroup Completion Problems, page 3 # The solution is unique: # 1 3 2 5 4 # 2 5 4 1 3 # 4 1 3 2 5 # 5 4 1 3 2 # 3 2 5 4 1 # def puzzle(1) do [[1, 0, 0, 0, 4], # 0 are the unknowns [0, 5, 0, 0, 0], [4, 0, 0, 2, 0], [0, 4, 0, 0, 0], [0, 0, 5, 0, 1]] end # # Example from Gomes & Shmoys, page 3. # Solution: # 4 1 2 3 # 2 3 4 1 # 1 4 3 2 # 3 2 1 4 # def puzzle(2) do [[0, 1, 2, 3], [2, 0, 4, 1], [1, 4, 0, 2], [3, 0, 1, 0]] end # Example from Gomes & Shmoys, page 7 # Two solutions. # def puzzle(3) do [[0, 1, 0, 0], [0, 0, 2, 0], [0, 3, 0, 0], [0, 0, 0, 4]] end # # Example from Global Constraint Catalogue # http://www.emn.fr/x-info/sdemasse/gccat/sec2.7.108.html # # 12 solutions. # def puzzle(4) do [[1, 0, 0, 0], [0, 0, 0, 3], [3, 0, 0, 0], [0, 0, 0, 1]] end # # Problem from http://www.cs.cornell.edu/gomes/QUASIdemo.html # (n = 10] # Pattern #1. # There are _many_ solutions to this problem. # def puzzle(5) do [ [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 2, 0, 0, 0, 0], [1, 0, 0, 0, 2, 0, 0, 0, 0, 0], [0, 0, 0, 2, 1, 0, 0, 0, 0, 0], [0, 0, 2, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 2, 0], [0, 0, 0, 0, 0, 0, 0, 2, 0, 0] ] end # # Problem from http://www.cs.cornell.edu/gomes/QUASIdemo.html # (n = 10] # Pattern #2. # There are many solutions to this problem. # def puzzle(6) do [ [0, 0, 1, 2, 3, 4, 0, 0, 0, 0], [0, 1, 2, 3, 0, 0, 4, 0, 0, 0], [1, 2, 3, 0, 0, 0, 0, 4, 0, 0], [2, 3, 0, 0, 0, 0, 0, 0, 4, 0], [3, 0, 0, 0, 0, 0, 0, 0, 0, 4], [5, 6, 0, 0, 0, 0, 0, 0, 0, 0], [0, 5, 6, 0, 0, 0, 0, 0, 0, 0], [0, 0, 5, 6, 0, 0, 0, 0, 0, 0], [0, 0, 0, 5, 6, 0, 0, 0, 0, 0], [0, 0, 0, 0, 5, 6, 0, 0, 0, 0] ] end # # Problem from http://www.cs.cornell.edu/gomes/QUASIdemo.html # (n = 10] # Pattern #3. # Coding: # dark red = 1 # light blue = 2 # dark blue = 3 # light red = 4 # brown = 5 # green = 6 # pink = 7 # grey = 8 # black = 9 # yellow = 10 # There are 40944 solutions for this pattern. # # This takes 9.5s, about to solve and print all solutions. # def puzzle(7) do [ [0, 0, 1, 5, 2, 6, 7, 8, 0, 0], [0, 1, 5, 2, 0, 0, 6, 7, 8, 0], [1, 5, 2, 0, 0, 0, 0, 6, 7, 8], [5, 2, 0, 0, 0, 0, 0, 0, 6, 7], [2, 0, 0, 0, 0, 0, 0, 0, 0, 6], [4, 10, 0, 0, 0, 0, 0, 0, 3, 9], [0, 4, 10, 0, 0, 0, 0, 3, 9, 0], [0, 0, 4, 10, 0, 0, 3, 9, 0, 0], [0, 0, 0, 4, 10, 3, 9, 0, 0, 0], [0, 0, 0, 0, 4, 9, 0, 0, 0, 0] ] end # # Problem from http://www.cs.cornell.edu/gomes/QUASIdemo.html # (n = 10] # Pattern #4. # dark red = 1 # light blue = 2 # dark blue = 3 # light red = 4 # Note: There are no solutions to this problem. # def puzzle(8) do [ [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [2, 1, 0, 0, 0, 0, 0, 0, 0, 4], [3, 2, 1, 0, 0, 0, 0, 0, 4, 0], [0, 3, 2, 1, 0, 0, 0, 4, 0, 0], [0, 0, 3, 2, 1, 0, 4, 0, 0, 0], [0, 0, 0, 3, 2, 1, 0, 0, 0, 0], [0, 0, 0, 0, 3, 2, 1, 0, 0, 0], [0, 0, 0, 4, 0, 3, 2, 1, 0, 0], [0, 0, 4, 0, 0, 0, 3, 2, 1, 0], [0, 4, 0, 0, 0, 0, 0, 3, 2, 1] ] end # # Problem from http://www.cs.cornell.edu/gomes/QUASIdemo.html # (n = 10] # Pattern #5 # Note: There are no solutions to this problem. # def puzzle(9) do [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 2, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0] ] end # # # def run(opts \\ []) do # Problem 5..7 yields a huge number of solutions. # Let's just pick the first. 1..7 |> Enum.map(fn p -> IO.puts("Running problem #{p}") cond do p in [5..7] -> solve_puzzle(p, Keyword.put(opts, :num_solutions, 1)) true -> solve_puzzle(p, opts) end end) end def solve_puzzle(id, opts \\ []) do quasigroup_completion(puzzle(id), opts) end # # Running problems 8 and 9 (no solution) # Takes long time in current Fixpoint version ## ## Boris Okner: if using AllDifferent.DC.Fast for `latin_square`, ## these puzzles will instantly fail due to early propagation. def run_unsatisfiable(opts \\ []) do Enum.each([8, 9], fn puzzle_id -> try do solve_puzzle(puzzle_id, opts) catch {:fail, _} -> IO.puts("Puzzle #{puzzle_id} doesn't have solutions") end end) end @doc """ quasigroup_completion(mat,num_sols \\ :infinity) Solves the Quasigroup completion problem for the matrix `mat`. `num_sols` are the required number of solutions, defaults to :infinity. """ def quasigroup_completion(mat, opts \\ []) do n = length(mat) dom = 1..n # # Decision variables # x = for i <- 0..(n - 1) do for j <- 0..(n - 1) do v = mat_at(mat, i, j) if v > 0 do # > 0: this is a hint IntVariable.new(v, name: "x[#{i},#{j}]") else # 0: unknown IntVariable.new(dom, name: "x[#{i},#{j}]") end end end x_flatten = List.flatten(x) # # Constraints # constraints = latin_square(x) model = Model.new( x_flatten, constraints ) Logger.configure(level: :info) opts = Keyword.merge(default_opts(), opts) |> then(fn opts -> Keyword.put(opts, :stop_on, {:max_solutions, opts[:num_solutions]}) end) {:ok, result} = CPSolver.solve( model, opts ) ## Print last solution print_matrix(result.solutions |> List.last(), n, n, "~3w") ## IO.inspect(result.statistics) {:ok, result} end defp default_opts() do [ search: {:first_fail, :indomain_max}, # search: {:first_fail, :indomain_min}, # search: {:first_fail, :indomain_random}, # search: {:input_order, :indomain_max}, num_solutions: :infinity, timeout: :timer.minutes(5) ] end end