lib/examples/knapsack.ex

defmodule CPSolver.Examples.Knapsack do
  @moduledoc """
  The 'knapsack' problem is mathematically formulated in
  the following way. Given n items to choose from, each item i ∈ 0 ...n − 1 has a value v[i] and a
  weight w[i]. The knapsack has a limited capacity K. Let x[i] be a variable that is 1 if you choose
  to take item i and 0 if you leave item i behind. Then the knapsack problem is formalized as the
  following optimization problem:

  Maximize sum(x[i]*v[i]), i = 0 ... n - 1
  subject to sum(x[i] * w[i]) <= K

  Input data:
  First line: "n K"
  Next lines: "value weight"
  """
  alias CPSolver.IntVariable, as: Variable
  alias CPSolver.Model
  alias CPSolver.Constraint.Sum
  alias CPSolver.Constraint.LessOrEqual
  import CPSolver.Variable.View.Factory
  alias CPSolver.Objective

  def prebuild_model(values, weights, capacity) do
    items = Enum.map(1..length(values), fn i -> Variable.new(0..1, name: "item_#{i}") end)
    total_weight = Variable.new(0..capacity, name: "total_weight")
    total_value = Variable.new(0..Enum.sum(values), name: "total_value")

    weight_views =
      Enum.map(
        Enum.zip(items, weights),
        fn {item, weight} -> mul(item, weight) end
      )

    value_views =
      Enum.map(
        Enum.zip(items, values),
        fn {item, value} -> mul(item, value) end
      )

    %{
      items: items,
      total_value: total_value,
      total_weight: total_weight,
      weight_views: weight_views,
      value_views: value_views
    }
  end

  def value_maximization_model(values, weights, capacity) do
    %{
      items: items,
      total_value: total_value,
      total_weight: total_weight,
      weight_views: weight_views,
      value_views: value_views
    } =
      prebuild_model(values, weights, capacity)

    constraints = [
      Sum.new(
        total_weight,
        weight_views
      ),
      Sum.new(total_value, value_views),
      LessOrEqual.new(total_weight, capacity)
    ]

    Model.new(
      items ++ [total_weight, total_value],
      constraints,
      objective: Objective.maximize(total_value)
    )
  end

  def model(input, kind \\ :value_maximization) do
    input
    |> File.read!()
    |> String.trim()
    |> String.split("\n")
    |> then(fn lines ->
      [header | item_data] = lines
      capacity = String.split(header, " ") |> List.last() |> String.to_integer()

      {values, weights} =
        List.foldr(item_data, {[], []}, fn str, {vals_acc, weights_acc} ->
          [v, w] = String.split(str, " ")

          {
            [String.to_integer(v) | vals_acc],
            [String.to_integer(w) | weights_acc]
          }
        end)

      model(values, weights, capacity, kind)
    end)
  end

  def model(values, weights, capacity, kind \\ :value_maximization)

  def model(values, weights, capacity, :value_maximization) do
    value_maximization_model(values, weights, capacity)
  end

  def model(values, weights, capacity, :free_space_minimization) do
    free_space_minimization_model(values, weights, capacity)
  end

  ## Minimizes free space
  def free_space_minimization_model(values, weights, capacity) do
    %{
      items: items,
      total_weight: total_weight,
      weight_views: weight_views
    } =
      prebuild_model(values, weights, capacity)

    space_constraints = [
      Sum.new(total_weight, weight_views),
      LessOrEqual.new(total_weight, capacity)
    ]

    Model.new(
      items ++ [total_weight],
      space_constraints,
      objective: Objective.minimize(linear(total_weight, -1, capacity))
    )
  end

  def check_solution(solution, optimal, values, weights, capacity) do
    items_to_pack = Enum.take(solution, length(weights))
    total_weight = sum_products(items_to_pack, weights)

    total_weight <= capacity && sum_products(items_to_pack, values) <= optimal
  end

  defp sum_products(list1, list2) do
    Enum.zip(list1, list2)
    |> Enum.reduce(0, fn {el1, el2}, acc -> acc + el1 * el2 end)
  end

  @doc """
  From Rosetta code:
  http://rosettacode.org/wiki/Knapsack_problem/0-1

  A tourist wants to make a good trip at the weekend with his friends. They
  will go to the mountains to see the wonders of nature, so he needs to
  pack well for the trip. He has a good knapsack for carrying things, but
  knows that he can carry a maximum of only 4kg in it and it will have
  to last the whole day. He creates a list of what he wants to bring for the
  trip but the total weight of all items is too much. He then decides to
  add columns to his initial list detailing their weights and a numerical
  value representing how important the item is for the trip.

  Here is the list:
  Table of potential knapsack items
  item 	weight (dag) 	value
  map 	9 	150
  compass 	13 	35
  water 	153 	200
  sandwich 	50 	160
  glucose 	15 	60
  tin 	68 	45
  banana 	27 	60
  apple 	39 	40
  cheese 	23 	30
  beer 	52 	10
  suntan cream 	11 	70
  camera 	32 	30
  T-shirt 	24 	15
  trousers 	48 	10
  umbrella 	73 	40
  waterproof trousers 	42 	70
  waterproof overclothes 	43 	75
  note-case 	22 	80
  sunglasses 	7 	20
  towel 	18 	12
  socks 	4 	50
  book 	30 	10
  knapsack 	<=400 dag 	 ?

  The tourist can choose to take any combination of items from the list, but
  only one of each item is available. He may not cut or diminish the items,
  so he can only take whole units of any item.

  Which items does the tourist carry in his knapsack so that their total weight
  does not exceed 400 dag [4 kg], and their total value is maximised?

  [dag = decagram = 10 grams]
  """
  def tourist_knapsack_model() do
    items = [
      {:map, 9, 150},
      {:compass, 13, 35},
      {:water, 153, 200},
      {:sandwich, 50, 160},
      {:glucose, 15, 60},
      {:tin, 68, 45},
      {:banana, 27, 60},
      {:apple, 39, 40},
      {:cheese, 23, 30},
      {:beer, 52, 10},
      {:suntan_cream, 11, 70},
      {:camera, 32, 30},
      {:t_shirt, 24, 15},
      {:trousers, 48, 10},
      {:umbrella, 73, 40},
      {:waterproof_trousers, 42, 70},
      {:waterproof_overclothes, 43, 75},
      {:note_case, 22, 80},
      {:sunglasses, 7, 20},
      {:towel, 18, 12},
      {:socks, 4, 50},
      {:book, 30, 10}
    ]

    capacity = 400

    {item_names, weights, values} =
      List.foldr(items, {[], [], []}, fn {n, w, v}, {n_acc, w_acc, v_acc} = _acc ->
        {[n | n_acc], [w | w_acc], [v | v_acc]}
      end)

    model(values, weights, capacity)
    |> replace_variable_names(item_names)
  end

  defp replace_variable_names(%{variables: variables} = model, item_names) do
    variables
    |> Enum.zip(item_names ++ List.duplicate(nil, length(variables) - length(item_names)))
    |> Enum.map(fn {var, name} -> (name && Map.put(var, :name, name)) || var end)
    |> then(fn named_vars -> Map.put(model, :variables, named_vars) end)
  end
end