lib/examples/hakank/survo_puzzle.ex

#
# Survo puzzle in Elixir.
#
# http://en.wikipedia.org/wiki/Survo_Puzzle
# """
# Survo puzzle is a kind of logic puzzle presented (in April 2006) and studied
# by Seppo Mustonen. The name of the puzzle is associated to Mustonen's
# Survo system which is a general environment for statistical computing and
# related areas.

# In a Survo puzzle the task is to fill an m * n table by integers 1,2,...,m*n so
# that each of these numbers appears only once and their row and column sums are
# equal to integers given on the bottom and the right side of the table.
# Often some of the integers are given readily in the table in order to
# guarantee uniqueness of the solution and/or for making the task easier.
# """
#
# See also
# http://www.survo.fi/english/index.html
# http://www.survo.fi/puzzles/index.html
#
#
# 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.SurvoPuzzle do

  # import Enum # Conflicts with CPSolver.Constraint.Factory.sum
  import Hakank.CPUtils

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

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


  def print_solution(x,rows, cols,rowsums,colsums) do
    for i <- 0..rows-1 do
      for j <- 0..cols-1 do
        :io.format("~3w",[Enum.at(x,i*cols+j)])
      end
      :io.format(" = ~3w ~n",[Enum.at(rowsums,i)])
    end
    for j <- 0..cols-1 do
      :io.format("~3w",[Enum.at(colsums,j)])
    end
    :io.format("~n~n",[])
  end

  #
  # From http://en.wikipedia.org/wiki/Survo_Puzzle, first example
  #
  # Solutions should be
  #
  # 12  6  2 10  = 30
  #  8  1  5  4  = 18
  #  7  9  3 11  = 30
  # 27 16 10 25
  #
  # I.e 12  6  2 10  8  1  5  4  7  9  3 11
  #
  def puzzle(1) do
    rowsums = [30,18,30]
    colsums = [27,16,10,25]
    # 0 is unknown -> to be decided
    problem = [[0, 6, 0, 0],  # 0 means unknown
               [8, 0, 0, 0],
               [0, 0, 3, 0]]
    [rowsums,colsums,problem]
  end

  #
  # From http://en.wikipedia.org/wiki/Survo_Puzzle, second example
  # difficulty 0

  def puzzle(2) do
    rowsums = [9, 12]
    colsums = [9, 7, 5]
    problem = [[0, 0, 3],
               [0, 6, 0]]
    [rowsums,colsums,problem]
  end

  # http://en.wikipedia.org/wiki/Survo_Puzzle, third example
  # difficulty 150 ("open puzzle", i.e. no hints}
  # It's an unique solution.
  # (817 propagations with Gecode/fz, and 33 failures, 88 commits}
  # r = 3;
  # c = 4;
  # rowsums = [24,15,39];
  # colsums = [21,10,18,29];
  # matrix = array2d(1..r, 1..c,
  #   [
  #     0, 0, 0, 0,
  #     0, 0, 0, 0,
  #     0, 0, 0, 0
  #   ]};
  # Note: this version has no hints
  def puzzle(3) do
    rowsums = [24,15,39]
    colsums = [21,10,18,29]
    problem = [[0, 0, 0, 0],
               [0, 0, 0, 0],
               [0, 0, 0, 0]]
    [rowsums,colsums,problem]
  end



  # same as above but with hints: difficulty 0
  # (15 propagations with Gecode/fz, no failures, no commits]
  # matrix = array2d(1..r, 1..c,
  #    [
  #      7, 0, 5, 0,
  #      0, 1, 0, 8,
  #      0, 0, 11, 0
  #    ]];
  def puzzle(4) do
    rowsums = [24,15,39]
    colsums = [21,10,18,29]
    problem = [[7, 0, 5, 0],
               [0, 1, 0, 8],
               [0, 0, 11, 0]]
    [rowsums,colsums,problem]
  end



  # http://www.survo.fi/puzzles/280708.txt, third puzzle
  # Survo puzzle 128/2008 (1700] #364-35846
  #
  #    A  B  C  D  E  F
  # 1  *  *  *  *  *  * 30
  # 2  *  * 18  *  *  * 86
  # 3  *  *  *  *  *  * 55
  #   22 11 42 32 27 37
  #
  # Solution:
  #  4  1 10  5  3  7 =  30
  # 12  8 18 16 15 17 =  86
  #  6  2 14 11  9 13 =  55
  # 22 11 42 32 27 37
  #
  def puzzle(5) do
    rowsums = [30, 86, 55]
    colsums = [22, 11, 42, 32, 27, 37]
    problem = [[0, 0,  0, 0, 0, 0],
               [0, 0, 18, 0, 0, 0],
               [0, 0,  0, 0, 0, 0]]
    [rowsums,colsums,problem]
  end

  #
  # http://en.wikipedia.org/wiki/Survo_Puzzle, under "Swapping method"
  # (open puzzle]
  #
  # Solution:
  # 15 16 12  8 =  51
  # 14 11  7  4 =  36
  # 13 10  6  3 =  32
  #  9  5  1  2 =  17
  # 51 42 26 17

  def puzzle(6) do
    rowsums = [51,36,32,17]
    colsums = [51,42,26,17]
    problem = [[0, 0, 0, 0],
               [0, 0, 0, 0],
               [0, 0, 0, 0],
               [0, 0, 0, 0]]
    [rowsums,colsums,problem]
  end


  def run() do

    for p <- 1..6 do
      IO.puts("\nPuzzle ##{p}")
      [rowsums,colsums,problem] = puzzle(p)
      survo_puzzle(rowsums,colsums,problem)
    end
  end

  def survo_puzzle(rowsums,colsums,problem) do
    rows = length(rowsums)
    cols = length(colsums)
    dom = 1..rows*cols

    # Decision variables
    x = for i <- 0..rows-1 do
          for j <- 0..cols-1 do
             v = mat_at(problem,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 = x |> List.flatten

    #
    # Constraints
    #

    all_different_constraint = AllDifferent.new(x_flatten)

    # Row constraints
    row_constraints = for {s,row} <- Enum.zip(rowsums,x) do
      Sum.new(s,row)
    end

    # Column constraints
    col_constraints = for {s,col} <- Enum.zip(colsums, transpose(x)) do
      Sum.new(s,col)
    end

    constraints = [all_different_constraint] ++ row_constraints ++ col_constraints

    model = Model.new(x_flatten,
                      constraints
    )

    Logger.configure(level: :info)
    {:ok, result} =
      CPSolver.solve(model,
        search: {:first_fail, :indomain_min},
        # stop_on: {:max_solutions, 1},
        timeout: :infinity,
        space_threads: 12
      )

    # IO.inspect(result.solutions)
    # IO.inspect(Enum.map(result.solutions, fn sol -> Enum.zip(result.variables, sol) end))
    IO.inspect(result.statistics)

    result.solutions
    |> Enum.map(fn s -> s |> Enum.take(rows*cols) end)
    |> Enum.map(fn s -> print_solution(s,rows,cols,rowsums,colsums) end)

    # result.statistics

  end

end