lib/examples/hakank/coins_grid.ex

#
# Coins grid problem in Elixir.
#
# Problem from
# Tony Hurlimann: "A coin puzzle - SVOR-contest 2007"
# http://www.svor.ch/competitions/competition2007/AsroContestSolution.pdf
# """
# In a quadratic grid (or a larger chessboard) with 31x31 cells, one
# should place coins in such a way that the following conditions are
# fulfilled:
#   1. In each row exactly 14 coins must be placed.
#   2. In each column exactly 14 coins must be placed.
#   3. The sum of the quadratic horizontal distance from the main
#      diagonal of all cells containing a coin must be as small as possible.
#   4. In each cell at most one coin can be placed.
#
#  The description says to place 14x31 = 434 coins on the chessboard
#  each row containing 14 coins and each column also containing 14 coins.
# """
#
# Note: This problem is quite/very hard for (plain) CP solvers. A MIP solver solves
# the 14,31 problem in millis.
#
#
# Cf the MiniZinc model http://hakank.org/minizinc/coins_grid.mzn
#
#
# 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 naming and result handling.
##
defmodule CPSolver.Examples.Hakank.CoinsGrid do
  alias Hakank.CPUtils

  alias CPSolver.IntVariable
  alias CPSolver.Constraint.Sum
  # alias CPSolver.Constraint.Equal
  # alias CPSolver.Constraint.AllDifferent.FWC, as: AllDifferent
  alias CPSolver.Model
  alias CPSolver.Objective

  # import CPSolver.Constraint.Factory
  import CPSolver.Variable.View.Factory

  require Logger

  def run(opts \\ []) do
    run(31, 14, opts)
  end

  def run_7_3(opts) do
    run(7, 3, opts)
  end

  def run(n, c, opts \\ []) do
    Logger.configure(level: :info)
    opts =
      Keyword.merge(
      default_opts()
      |> Keyword.put(:solution_handler,
        fn solution -> print_solution(solution, n, false)
      end),
      opts
    )
    Logger.info("Started with n = #{n}, c = #{c}")
    # 7
    ##n =
    #  31

    # 3
    ##c =
    ##  14

    rs = 0..(n - 1)

    x =
      for i <- rs do
        for j <- rs do
          IntVariable.new(0..1, name: "x[#{i},#{j}]")
        end
      end

    x_flatten = List.flatten(x)

    # To be minimized
    z = IntVariable.new(0..(n * n * n), name: "z")

    # quadratic horizontal distance
    # MiniZinc: z = sum(i,j in 1..n) (  x[i,j]*(abs(i-j))*(abs(i-j))  )
    sum_constraint =
      Sum.new(
        z,
        for i <- rs, j <- rs do
          mul(CPUtils.mat_at(x, i, j), abs(i - j) ** 2)
        end
      )

    # MiniZinc: forall(i in 1..n) (  sum(j in 1..n) (x[i,j]) = c  )
    row_constraints =
      for row <- x do
        Sum.new(c, row)
      end

    # MiniZinc: forall(j in 1..n) (  sum(i in 1..n) (x[i,j]) = c  )
    col_constraints =
      for col <- CPUtils.transpose(x) do
        Sum.new(c, col)
      end

    # |> List.flatten
    constraints = [sum_constraint] ++ row_constraints ++ col_constraints
    # constraints = row_constraints ++ col_constraints

    model =
      Model.new(
        [z | x_flatten],
        constraints,
        # minimize z
        objective: Objective.minimize(z)
      )

    {:ok, res} =
      CPSolver.solve(
        model,
        opts)


    IO.inspect(res.statistics)
    best_solution = List.last(res.solutions)

    if best_solution do
      print_solution(best_solution, n, true)
    else
      Logger.info("No solutions found")
    end
  end

  defp print_solution(solution, n, print_matrix?) do
    solution = Enum.map(solution,
    fn {_name, value} ->
      value
      value when is_integer(value) -> value
    end)
    coins = hd(solution)
    matrix = tl(solution) |> Enum.take(n * n)
    Logger.info("coins: #{coins}")

    print_matrix? &&
      Enum.chunk_every(matrix, n)
      |> Enum.map(fn row -> Enum.join(row, " ") end)
      |> Enum.join("\n")
      |> then(fn matrix_str ->
        IO.puts("\nmatrix:\n" <> matrix_str <> "\n")
      end)
  end

  defp default_opts() do
      [
        n: 31, c: 14,
      search: {:first_fail, :indomain_max},
      #search: {:input_order, :indomain_max},
      # search: {:most_constrained, :indomain_max},
      # search: {:dom_deg, :indomain_max},
      # search: {:first_fail, :indomain_min},
      # search: {:max_regret, :indomain_max},
      #search: {:first_fail, :indomain_random},
      space_threads: 8,
      timeout: :timer.hours(1),
      # stop_on: {:max_solutions, 1},
    ]
  end
end