lib/examples/euler43.ex

defmodule CPSolver.Examples.Euler43 do
  @moduledoc """
  https://projecteuler.net/problem=43
  """

  @doc """
  int: n = 10;
  array[1..n] of var 0..9: x;
  array[int] of int: primes = [2,3,5,7,11,13,17];
  solve satisfy;
  constraint
    all_different(x) /\
    forall(i in 2..8) (
      (100*x[i] + 10*x[i+1] + x[i+2]) mod primes[i-1] = 0
    )
  ;
  output [ show(x),"\n"];
  """

  alias CPSolver.IntVariable, as: Variable
  alias CPSolver.Constraint.AllDifferent.FWC, as: AllDifferent
  alias CPSolver.Constraint.Modulo
  alias CPSolver.Model
  import CPSolver.Constraint.Factory
  import CPSolver.Variable.View.Factory

  require Logger

  @minizinc_solutions Enum.sort([
                        [4, 1, 6, 0, 3, 5, 7, 2, 8, 9],
                        [1, 4, 6, 0, 3, 5, 7, 2, 8, 9],
                        [4, 1, 0, 6, 3, 5, 7, 2, 8, 9],
                        [1, 4, 0, 6, 3, 5, 7, 2, 8, 9],
                        [4, 1, 3, 0, 9, 5, 2, 8, 6, 7],
                        [1, 4, 3, 0, 9, 5, 2, 8, 6, 7]
                      ])

  def model() do
    primes = [2, 3, 5, 7, 11, 13, 17]
    domain = 0..9

    x = Enum.map(1..10, fn i -> Variable.new(domain, name: "x#{i}") end)

    all_different_constraint = AllDifferent.new(x)

    constraints =
      Enum.reduce(2..8, [all_different_constraint], fn i, constraints_acc ->
        x_i = Enum.at(x, i - 1)
        x_i_1 = Enum.at(x, i)
        x_i_2 = Enum.at(x, i + 1)
        prime = Enum.at(primes, i - 2)
        {sum_var, sum_constraint} = sum([mul(x_i, 100), mul(x_i_1, 10), x_i_2])
        mod_constraint = Modulo.new(0, sum_var, prime)
        [sum_constraint, mod_constraint | constraints_acc]
      end)

    Model.new(x, constraints)
  end

  def check_solution(solution) do
    solution
    |> Enum.take(10)
    |> Kernel.in(@minizinc_solutions)
  end

  def run(opts \\ [search: {:input_order, :indomain_random}, space_threads: 8]) do
    {:ok, res} = CPSolver.solve(model(), opts)

    Enum.sort(Enum.map(res.solutions, fn s -> Enum.take(s, 10) end))
    |> tap(fn sorted_solutions ->
      (sorted_solutions == @minizinc_solutions &&
         Logger.notice("Solutions correspond to the ones given by MinZinc")) ||
        Logger.error("Solutions do not match MiniZinc")
    end)
    |> Enum.reduce(0, fn s, sum_acc -> Integer.undigits(s) + sum_acc end)
  end
end