{"id":13411840,"url":"https://github.com/ankane/or-tools-ruby","last_synced_at":"2025-11-17T14:14:13.966Z","repository":{"id":47322668,"uuid":"240172859","full_name":"ankane/or-tools-ruby","owner":"ankane","description":"Operations research tools for Ruby","archived":false,"fork":false,"pushed_at":"2025-05-13T04:41:10.000Z","size":531,"stargazers_count":187,"open_issues_count":5,"forks_count":29,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-05-13T05:28:14.448Z","etag":null,"topics":["bin-packing","knapsack-problem","operations-research","optimization","routing","scheduling","tsp","vrp"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ankane.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-02-13T03:59:39.000Z","updated_at":"2025-05-13T04:41:13.000Z","dependencies_parsed_at":"2023-11-17T03:23:50.646Z","dependency_job_id":"1981086f-d761-4e98-a68f-bbbb42a49548","html_url":"https://github.com/ankane/or-tools-ruby","commit_stats":{"total_commits":490,"total_committers":11,"mean_commits":44.54545454545455,"dds":"0.026530612244897944","last_synced_commit":"12157b153b6898c10ad93b99f3a3b3b6fcb561fd"},"previous_names":[],"tags_count":45,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ankane%2For-tools-ruby","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ankane%2For-tools-ruby/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ankane%2For-tools-ruby/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ankane%2For-tools-ruby/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ankane","download_url":"https://codeload.github.com/ankane/or-tools-ruby/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254310515,"owners_count":22049469,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bin-packing","knapsack-problem","operations-research","optimization","routing","scheduling","tsp","vrp"],"created_at":"2024-07-30T20:01:17.448Z","updated_at":"2025-11-17T14:14:13.902Z","avatar_url":"https://github.com/ankane.png","language":"Ruby","readme":"# OR-Tools Ruby\n\n[OR-Tools](https://github.com/google/or-tools) - operations research tools - for Ruby\n\n[![Build Status](https://github.com/ankane/or-tools-ruby/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/or-tools-ruby/actions)\n\n## Installation\n\nAdd this line to your application’s Gemfile:\n\n```ruby\ngem \"or-tools\"\n```\n\nInstallation can take a few minutes as OR-Tools downloads and builds.\n\n## Getting Started\n\nHigher Level Interfaces\n\n- [Scheduling](#scheduling)\n- [Seating](#seating)\n- [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)\n- [Sudoku](#sudoku)\n\nMathOpt\n\n- [Basic example](#basic-example)\n\nLinear Optimization\n\n- [Solving an LP Problem](#solving-an-lp-problem)\n\nInteger Optimization\n\n- [Solving a MIP Problem](#solving-a-mip-problem)\n\nConstraint Optimization\n\n- [CP-SAT Solver](#cp-sat-solver)\n- [Solving a CP Problem](#solving-a-cp-problem)\n- [Cryptarithmetic](#cryptarithmetic)\n- [The N-queens Problem](#the-n-queens-problem)\n- [Setting Solver Limits](#setting-solver-limits)\n\nAssignment\n\n- [Solving an Assignment Problem](#solving-an-assignment-problem)\n- [Assignment with Teams of Workers](#assignment-with-teams-of-workers)\n- [Linear Sum Assignment Solver](#linear-sum-assignment-solver)\n\nRouting\n\n- [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp-1)\n- [Vehicle Routing Problem (VRP)](#vehicle-routing-problem-vrp)\n- [Capacity Constraints](#capacity-constraints)\n- [Pickups and Deliveries](#pickups-and-deliveries)\n- [Time Window Constraints](#time-window-constraints)\n- [Resource Constraints](#resource-constraints)\n- [Penalties and Dropping Visits](#penalties-and-dropping-visits)\n- [Routing Options](#routing-options)\n\nBin Packing\n\n- [The Knapsack Problem](#the-knapsack-problem)\n- [Multiple Knapsacks](#multiple-knapsacks)\n- [Bin Packing Problem](#bin-packing-problem)\n\nNetwork Flows\n\n- [Maximum Flows](#maximum-flows)\n- [Minimum Cost Flows](#minimum-cost-flows)\n- [Assignment as a Min Cost Flow Problem](#assignment-as-a-min-cost-flow-problem)\n\nScheduling\n\n- [Employee Scheduling](#employee-scheduling)\n- [The Job Shop Problem](#the-job-shop-problem)\n\nOther Examples\n\n- [Sudoku](#sudoku-1)\n- [Wedding Seating Chart](#wedding-seating-chart)\n- [Set Partitioning](#set-partitioning)\n\n## Higher Level Interfaces\n\n### Scheduling\n\nSpecify people and their availabililty\n\n```ruby\npeople = [\n  {\n    availability: [\n      {starts_at: Time.parse(\"2025-01-01 08:00:00\"), ends_at: Time.parse(\"2025-01-01 16:00:00\")},\n      {starts_at: Time.parse(\"2025-01-02 08:00:00\"), ends_at: Time.parse(\"2025-01-02 16:00:00\")}\n    ],\n    max_hours: 40 # optional, applies to entire scheduling period\n  },\n  {\n    availability: [\n      {starts_at: Time.parse(\"2025-01-01 08:00:00\"), ends_at: Time.parse(\"2025-01-01 16:00:00\")},\n      {starts_at: Time.parse(\"2025-01-03 08:00:00\"), ends_at: Time.parse(\"2025-01-03 16:00:00\")}\n    ],\n    max_hours: 20\n  }\n]\n```\n\nSpecify shifts\n\n```ruby\nshifts = [\n  {starts_at: Time.parse(\"2025-01-01 08:00:00\"), ends_at: Time.parse(\"2025-01-01 16:00:00\")},\n  {starts_at: Time.parse(\"2025-01-02 08:00:00\"), ends_at: Time.parse(\"2025-01-02 16:00:00\")},\n  {starts_at: Time.parse(\"2025-01-03 08:00:00\"), ends_at: Time.parse(\"2025-01-03 16:00:00\")}\n]\n```\n\nRun the scheduler\n\n```ruby\nscheduler = ORTools::BasicScheduler.new(people: people, shifts: shifts)\n```\n\nThe scheduler maximizes the number of assigned hours. A person must be available for the entire shift to be considered for it.\n\nGet assignments (returns indexes of people and shifts)\n\n```ruby\nscheduler.assignments\n# [\n#   {person: 2, shift: 0},\n#   {person: 0, shift: 1},\n#   {person: 1, shift: 2}\n# ]\n```\n\nGet assigned hours and total hours\n\n```ruby\nscheduler.assigned_hours\nscheduler.total_hours\n```\n\nFeel free to create an issue if you have a scheduling use case that’s not covered.\n\n### Seating\n\nCreate a seating chart based on personal connections. Uses [this approach](https://www.improbable.com/news/2012/Optimal-seating-chart.pdf).\n\nSpecify connections\n\n```ruby\nconnections = [\n  {people: [\"A\", \"B\", \"C\"], weight: 2},\n  {people: [\"C\", \"D\", \"E\", \"F\"], weight: 1}\n]\n```\n\nUse different weights to prioritize seating. For a wedding, it may look like:\n\n```ruby\nconnections = [\n  {people: knows_partner1, weight: 1},\n  {people: knows_partner2, weight: 1},\n  {people: relationship1, weight: 100},\n  {people: relationship2, weight: 100},\n  {people: relationship3, weight: 100},\n  {people: friend_group1, weight: 10},\n  {people: friend_group2, weight: 10},\n  # ...\n]\n```\n\nIf two people have multiple connections, weights are added.\n\nSpecify tables and their capacity\n\n```ruby\ntables = [3, 3]\n```\n\nAssign seats\n\n```ruby\nseating = ORTools::Seating.new(connections: connections, tables: tables)\n```\n\nEach person will have a connection with at least one other person at their table.\n\nGet tables\n\n```ruby\nseating.assigned_tables\n```\n\nGet assignments by person\n\n```ruby\nseating.assignments\n```\n\nGet all connections for a person\n\n```ruby\nseating.connections_for(person)\n```\n\nGet connections for a person at their table\n\n```ruby\nseating.connections_for(person, same_table: true)\n```\n\n### Traveling Salesperson Problem (TSP)\n\nCreate locations - the first location will be the starting and ending point\n\n```ruby\nlocations = [\n  {name: \"Tokyo\", latitude: 35.6762, longitude: 139.6503},\n  {name: \"Delhi\", latitude: 28.7041, longitude: 77.1025},\n  {name: \"Shanghai\", latitude: 31.2304, longitude: 121.4737},\n  {name: \"São Paulo\", latitude: -23.5505, longitude: -46.6333},\n  {name: \"Mexico City\", latitude: 19.4326, longitude: -99.1332},\n  {name: \"Cairo\", latitude: 30.0444, longitude: 31.2357},\n  {name: \"Mumbai\", latitude: 19.0760, longitude: 72.8777},\n  {name: \"Beijing\", latitude: 39.9042, longitude: 116.4074},\n  {name: \"Dhaka\", latitude: 23.8103, longitude: 90.4125},\n  {name: \"Osaka\", latitude: 34.6937, longitude: 135.5023},\n  {name: \"New York City\", latitude: 40.7128, longitude: -74.0060},\n  {name: \"Karachi\", latitude: 24.8607, longitude: 67.0011},\n  {name: \"Buenos Aires\", latitude: -34.6037, longitude: -58.3816}\n]\n```\n\nLocations can have any fields - only `latitude` and `longitude` are required\n\nGet route\n\n```ruby\ntsp = ORTools::TSP.new(locations)\ntsp.route # [{name: \"Tokyo\", ...}, {name: \"Osaka\", ...}, ...]\n```\n\nGet distances between locations on route\n\n```ruby\ntsp.distances # [392.441, 1362.926, 1067.31, ...]\n```\n\nDistances are in kilometers - multiply by `0.6214` for miles\n\nGet total distance\n\n```ruby\ntsp.total_distance\n```\n\n### Sudoku\n\nCreate a puzzle with zeros in empty cells\n\n```ruby\ngrid = [\n  [0, 6, 0, 0, 5, 0, 0, 2, 0],\n  [0, 0, 0, 3, 0, 0, 0, 9, 0],\n  [7, 0, 0, 6, 0, 0, 0, 1, 0],\n  [0, 0, 6, 0, 3, 0, 4, 0, 0],\n  [0, 0, 4, 0, 7, 0, 1, 0, 0],\n  [0, 0, 5, 0, 9, 0, 8, 0, 0],\n  [0, 4, 0, 0, 0, 1, 0, 0, 6],\n  [0, 3, 0, 0, 0, 8, 0, 0, 0],\n  [0, 2, 0, 0, 4, 0, 0, 5, 0]\n]\nsudoku = ORTools::Sudoku.new(grid)\nsudoku.solution\n```\n\nIt can also solve more advanced puzzles like [The Miracle](https://www.youtube.com/watch?v=yKf9aUIxdb4)\n\n```ruby\ngrid = [\n  [0, 0, 0, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0],\n  [0, 0, 1, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 2, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0]\n]\nsudoku = ORTools::Sudoku.new(grid, anti_knight: true, anti_king: true, non_consecutive: true)\nsudoku.solution\n```\n\nAnd [this 4-digit puzzle](https://www.youtube.com/watch?v=hAyZ9K2EBF0)\n\n```ruby\ngrid = [\n  [0, 0, 0, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0],\n  [3, 8, 4, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 2]\n]\nsudoku = ORTools::Sudoku.new(grid, x: true, anti_knight: true, magic_square: true)\nsudoku.solution\n```\n\n## MathOpt\n\n### Basic Example\n\n[Guide](https://developers.google.com/optimization/math_opt/basic_example)\n\n```ruby\n# build the model\nmodel = ORTools::MathOpt::Model.new(\"getting_started_lp\")\nx = model.add_variable(-1.0, 1.5, \"x\")\ny = model.add_variable(0.0, 1.0, \"y\")\nmodel.add_linear_constraint(x + y \u003c= 1.5)\nmodel.maximize(x + 2 * y)\n\n# solve\nresult = model.solve\n\n# inspect the solution\nputs \"Objective value: #{result.objective_value}\"\nputs \"x: #{result.variable_values[x]}\"\nputs \"y: #{result.variable_values[y]}\"\n```\n\n## Linear Optimization\n\n### Solving an LP Problem\n\n[Guide](https://developers.google.com/optimization/lp/lp_example)\n\n```ruby\n# declare the solver\nsolver = ORTools::Solver.new(\"GLOP\")\n\n# create the variables\nx = solver.num_var(0, solver.infinity, \"x\")\ny = solver.num_var(0, solver.infinity, \"y\")\nputs \"Number of variables = #{solver.num_variables}\"\n\n# define the constraints\nsolver.add(x + 2 * y \u003c= 14)\nsolver.add(3 * x - y \u003e= 0)\nsolver.add(x - y \u003c= 2)\nputs \"Number of constraints = #{solver.num_constraints}\"\n\n# define the objective function\nsolver.maximize(3 * x + 4 * y)\n\n# invoke the solver\nstatus = solver.solve\n\n# display the solution\nif status == :optimal\n  puts \"Solution:\"\n  puts \"Objective value = #{solver.objective.value}\"\n  puts \"x = #{x.solution_value}\"\n  puts \"y = #{y.solution_value}\"\nelse\n  puts \"The problem does not have an optimal solution.\"\nend\n```\n\n## Integer Optimization\n\n### Solving a MIP Problem\n\n[Guide](https://developers.google.com/optimization/mip/mip_example)\n\n```ruby\n# declare the MIP solver\nsolver = ORTools::Solver.new(\"CBC\")\n\n# define the variables\ninfinity = solver.infinity\nx = solver.int_var(0, infinity, \"x\")\ny = solver.int_var(0, infinity, \"y\")\n\nputs \"Number of variables = #{solver.num_variables}\"\n\n# define the constraints\nsolver.add(x + 7 * y \u003c= 17.5)\nsolver.add(x \u003c= 3.5)\n\nputs \"Number of constraints = #{solver.num_constraints}\"\n\n# define the objective\nsolver.maximize(x + 10 * y)\n\n# call the solver\nstatus = solver.solve\n\n# display the solution\nif status == :optimal\n  puts \"Solution:\"\n  puts \"Objective value = #{solver.objective.value}\"\n  puts \"x = #{x.solution_value}\"\n  puts \"y = #{y.solution_value}\"\nelse\n  puts \"The problem does not have an optimal solution.\"\nend\n```\n\n## Constraint Optimization\n\n### CP-SAT Solver\n\n[Guide](https://developers.google.com/optimization/cp/cp_solver)\n\n```ruby\n# declare the model\nmodel = ORTools::CpModel.new\n\n# create the variables\nnum_vals = 3\nx = model.new_int_var(0, num_vals - 1, \"x\")\ny = model.new_int_var(0, num_vals - 1, \"y\")\nz = model.new_int_var(0, num_vals - 1, \"z\")\n\n# create the constraint\nmodel.add(x != y)\n\n# call the solver\nsolver = ORTools::CpSolver.new\nstatus = solver.solve(model)\n\n# display the first solution\nif status == :optimal || status == :feasible\n  puts \"x = #{solver.value(x)}\"\n  puts \"y = #{solver.value(y)}\"\n  puts \"z = #{solver.value(z)}\"\nelse\n  puts \"No solution found.\"\nend\n```\n\n### Solving a CP Problem\n\n[Guide](https://developers.google.com/optimization/cp/cp_example)\n\n```ruby\n# declare the model\nmodel = ORTools::CpModel.new\n\n# create the variables\nvar_upper_bound = [50, 45, 37].max\nx = model.new_int_var(0, var_upper_bound, \"x\")\ny = model.new_int_var(0, var_upper_bound, \"y\")\nz = model.new_int_var(0, var_upper_bound, \"z\")\n\n# define the constraints\nmodel.add(x*2 + y*7 + z*3 \u003c= 50)\nmodel.add(x*3 - y*5 + z*7 \u003c= 45)\nmodel.add(x*5 + y*2 - z*6 \u003c= 37)\n\n# define the objective function\nmodel.maximize(x*2 + y*2 + z*3)\n\n# call the solver\nsolver = ORTools::CpSolver.new\nstatus = solver.solve(model)\n\n# display the solution\nif status == :optimal || status == :feasible\n  puts \"Maximum of objective function: #{solver.objective_value}\"\n  puts \"x = #{solver.value(x)}\"\n  puts \"y = #{solver.value(y)}\"\n  puts \"z = #{solver.value(z)}\"\nelse\n  puts \"No solution found.\"\nend\n```\n\n### Cryptarithmetic\n\n[Guide](https://developers.google.com/optimization/cp/cryptarithmetic)\n\n```ruby\n# define the variables\nmodel = ORTools::CpModel.new\n\nbase = 10\n\nc = model.new_int_var(1, base - 1, \"C\")\np = model.new_int_var(0, base - 1, \"P\")\ni = model.new_int_var(1, base - 1, \"I\")\ns = model.new_int_var(0, base - 1, \"S\")\nf = model.new_int_var(1, base - 1, \"F\")\nu = model.new_int_var(0, base - 1, \"U\")\nn = model.new_int_var(0, base - 1, \"N\")\nt = model.new_int_var(1, base - 1, \"T\")\nr = model.new_int_var(0, base - 1, \"R\")\ne = model.new_int_var(0, base - 1, \"E\")\n\nletters = [c, p, i, s, f, u, n, t, r, e]\n\n# define the constraints\nmodel.add_all_different(letters)\n\nmodel.add(c * base + p + i * base + s + f * base * base + u * base +\n  n == t * base * base * base + r * base * base + u * base + e)\n\n# define the solution printer\nclass VarArraySolutionPrinter \u003c ORTools::CpSolverSolutionCallback\n  attr_reader :solution_count\n\n  def initialize(variables)\n    super()\n    @variables = variables\n    @solution_count = 0\n  end\n\n  def on_solution_callback\n    @solution_count += 1\n    @variables.each do |v|\n      print \"%s=%i \" % [v.name, value(v)]\n    end\n    puts\n  end\nend\n\n# invoke the solver\nsolver = ORTools::CpSolver.new\nsolution_printer = VarArraySolutionPrinter.new(letters)\nstatus = solver.search_for_all_solutions(model, solution_printer)\n\nputs\nputs \"Statistics\"\nputs \"  - status          : %s\" % status\nputs \"  - conflicts       : %i\" % solver.num_conflicts\nputs \"  - branches        : %i\" % solver.num_branches\nputs \"  - wall time       : %f s\" % solver.wall_time\nputs \"  - solutions found : %i\" % solution_printer.solution_count\n```\n\n### The N-queens Problem\n\n[Guide](https://developers.google.com/optimization/cp/queens)\n\n```ruby\n# declare the model\nboard_size = 8\nmodel = ORTools::CpModel.new\n\n# create the variables\nqueens = board_size.times.map { |i| model.new_int_var(0, board_size - 1, \"x%i\" % i) }\n\n# create the constraints\nboard_size.times do |i|\n  diag1 = []\n  diag2 = []\n  board_size.times do |j|\n    q1 = model.new_int_var(0, 2 * board_size, \"diag1_%i\" % i)\n    diag1 \u003c\u003c q1\n    model.add(q1 == queens[j] + j)\n    q2 = model.new_int_var(-board_size, board_size, \"diag2_%i\" % i)\n    diag2 \u003c\u003c q2\n    model.add(q2 == queens[j] - j)\n  end\n  model.add_all_different(diag1)\n  model.add_all_different(diag2)\nend\n\n# create a solution printer\nclass SolutionPrinter \u003c ORTools::CpSolverSolutionCallback\n  attr_reader :solution_count\n\n  def initialize(variables)\n    super()\n    @variables = variables\n    @solution_count = 0\n  end\n\n  def on_solution_callback\n    @solution_count += 1\n    @variables.each do |v|\n      print \"%s = %i \" % [v.name, value(v)]\n    end\n    puts\n  end\nend\n\n# call the solver and display the results\nsolver = ORTools::CpSolver.new\nsolution_printer = SolutionPrinter.new(queens)\nstatus = solver.search_for_all_solutions(model, solution_printer)\nputs\nputs \"Solutions found : %i\" % solution_printer.solution_count\n```\n\n### Setting Solver Limits\n\n[Guide](https://developers.google.com/optimization/cp/cp_tasks)\n\n```ruby\n# create the model\nmodel = ORTools::CpModel.new\n\n# create the variables\nnum_vals = 3\nx = model.new_int_var(0, num_vals - 1, \"x\")\ny = model.new_int_var(0, num_vals - 1, \"y\")\nz = model.new_int_var(0, num_vals - 1, \"z\")\n\n# add an all-different constraint\nmodel.add(x != y)\n\n# create the solver\nsolver = ORTools::CpSolver.new\n\n# set a time limit of 10 seconds.\nsolver.parameters.max_time_in_seconds = 10\n\n# solve the model\nstatus = solver.solve(model)\n\n# display the first solution\nif status == :optimal\n  puts \"x = #{solver.value(x)}\"\n  puts \"y = #{solver.value(y)}\"\n  puts \"z = #{solver.value(z)}\"\nend\n```\n\n## Assignment\n\n### Solving an Assignment Problem\n\n[Guide](https://developers.google.com/optimization/assignment/assignment_example)\n\n```ruby\n# create the data\ncosts = [\n  [90, 80, 75, 70],\n  [35, 85, 55, 65],\n  [125, 95, 90, 95],\n  [45, 110, 95, 115],\n  [50, 100, 90, 100]\n]\nnum_workers = costs.length\nnum_tasks = costs[0].length\n\n# create the solver\nsolver = ORTools::Solver.new(\"CBC\")\n\n# create the variables\nx = {}\nnum_workers.times do |i|\n  num_tasks.times do |j|\n    x[[i, j]] = solver.int_var(0, 1, \"\")\n  end\nend\n\n# create the constraints\n# each worker is assigned to at most 1 task\nnum_workers.times do |i|\n  solver.add(num_tasks.times.sum { |j| x[[i, j]] } \u003c= 1)\nend\n\n# each task is assigned to exactly one worker\nnum_tasks.times do |j|\n  solver.add(num_workers.times.sum { |i| x[[i, j]] } == 1)\nend\n\n# create the objective function\nobjective_terms = []\nnum_workers.times do |i|\n  num_tasks.times do |j|\n    objective_terms \u003c\u003c (costs[i][j] * x[[i, j]])\n  end\nend\nsolver.minimize(objective_terms.sum)\n\n# invoke the solver\nstatus = solver.solve\n\n# print the solution\nif status == :optimal || status == :feasible\n  puts \"Total cost = #{solver.objective.value}\"\n  num_workers.times do |i|\n    num_tasks.times do |j|\n      # test if x[i,j] is 1 (with tolerance for floating point arithmetic)\n      if x[[i, j]].solution_value \u003e 0.5\n        puts \"Worker #{i} assigned to task #{j}. Cost = #{costs[i][j]}\"\n      end\n    end\n  end\nelse\n  puts \"No solution found.\"\nend\n```\n\n### Assignment with Teams of Workers\n\n[Guide](https://developers.google.com/optimization/assignment/assignment_teams#mip)\n\n```ruby\n# create the data\ncosts = [\n  [90, 76, 75, 70],\n  [35, 85, 55, 65],\n  [125, 95, 90, 105],\n  [45, 110, 95, 115],\n  [60, 105, 80, 75],\n  [45, 65, 110, 95]\n]\nnum_workers = costs.length\nnum_tasks = costs[1].length\n\nteam1 = [0, 2, 4]\nteam2 = [1, 3, 5]\nteam_max = 2\n\n# create the solver\nsolver = ORTools::Solver.new(\"CBC\")\n\n# create the variables\nx = {}\nnum_workers.times do |i|\n  num_tasks.times do |j|\n    x[[i, j]] = solver.bool_var(\"x[#{i},#{j}]\")\n  end\nend\n\n# add the constraints\n# each worker is assigned at most 1 task\nnum_workers.times do |i|\n  solver.add(num_tasks.times.sum { |j| x[[i, j]] } \u003c= 1)\nend\n\n# each task is assigned to exactly one worker\nnum_tasks.times do |j|\n  solver.add(num_workers.times.sum { |i| x[[i, j]] } == 1)\nend\n\n# each team takes at most two tasks\nsolver.add(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }.sum \u003c= team_max)\nsolver.add(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }.sum \u003c= team_max)\n\n# create the objective\nsolver.minimize(\n  num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * costs[i][j] } }.sum\n)\n\n# invoke the solver\nstatus = solver.solve\n\n# display the results\nif status == :optimal || status == :feasible\n  puts \"Total cost = #{solver.objective.value}\"\n  num_workers.times do |worker|\n    num_tasks.times do |task|\n      if x[[worker, task]].solution_value \u003e 0.5\n        puts \"Worker #{worker} assigned to task #{task}. Cost = #{costs[worker][task]}\"\n      end\n    end\n  end\nelse\n  puts \"No solution found.\"\nend\n```\n\n### Linear Sum Assignment Solver\n\n[Guide](https://developers.google.com/optimization/assignment/linear_assignment)\n\n```ruby\n# create the data\ncosts = [\n  [90, 76, 75, 70],\n  [35, 85, 55, 65],\n  [125, 95, 90, 105],\n  [45, 110, 95, 115],\n]\nnum_workers = costs.length\nnum_tasks = costs[0].length\n\n# create the solver\nassignment = ORTools::LinearSumAssignment.new\n\n# add the constraints\nnum_workers.times do |worker|\n  num_tasks.times do |task|\n    if costs[worker][task]\n      assignment.add_arc_with_cost(worker, task, costs[worker][task])\n    end\n  end\nend\n\n# invoke the solver\nstatus = assignment.solve\n\n# display the results\ncase status\nwhen :optimal\n  puts \"Total cost = #{assignment.optimal_cost}\"\n  assignment.num_nodes.times do |i|\n    puts \"Worker #{i} assigned to task #{assignment.right_mate(i)}. Cost = #{assignment.assignment_cost(i)}\"\n  end\nwhen :infeasible\n  puts \"No assignment is possible.\"\nwhen :possible_overflow\n  puts \"Some input costs are too large and may cause an integer overflow.\"\nend\n```\n\n## Routing\n\n### Traveling Salesperson Problem (TSP)\n\n[Guide](https://developers.google.com/optimization/routing/tsp.html)\n\n```ruby\n# create the data\ndata = {}\ndata[:distance_matrix] = [\n  [0, 2451, 713, 1018, 1631, 1374, 2408, 213, 2571, 875, 1420, 2145, 1972],\n  [2451, 0, 1745, 1524, 831, 1240, 959, 2596, 403, 1589, 1374, 357, 579],\n  [713, 1745, 0, 355, 920, 803, 1737, 851, 1858, 262, 940, 1453, 1260],\n  [1018, 1524, 355, 0, 700, 862, 1395, 1123, 1584, 466, 1056, 1280, 987],\n  [1631, 831, 920, 700, 0, 663, 1021, 1769, 949, 796, 879, 586, 371],\n  [1374, 1240, 803, 862, 663, 0, 1681, 1551, 1765, 547, 225, 887, 999],\n  [2408, 959, 1737, 1395, 1021, 1681, 0, 2493, 678, 1724, 1891, 1114, 701],\n  [213, 2596, 851, 1123, 1769, 1551, 2493, 0, 2699, 1038, 1605, 2300, 2099],\n  [2571, 403, 1858, 1584, 949, 1765, 678, 2699, 0, 1744, 1645, 653, 600],\n  [875, 1589, 262, 466, 796, 547, 1724, 1038, 1744, 0, 679, 1272, 1162],\n  [1420, 1374, 940, 1056, 879, 225, 1891, 1605, 1645, 679, 0, 1017, 1200],\n  [2145, 357, 1453, 1280, 586, 887, 1114, 2300, 653, 1272, 1017, 0, 504],\n  [1972, 579, 1260, 987, 371, 999, 701, 2099, 600, 1162, 1200, 504, 0]\n]\ndata[:num_vehicles] = 1\ndata[:depot] = 0\n\n# create the distance callback\nmanager = ORTools::RoutingIndexManager.new(data[:distance_matrix].length, data[:num_vehicles], data[:depot])\nrouting = ORTools::RoutingModel.new(manager)\n\ndistance_callback = lambda do |from_index, to_index|\n  from_node = manager.index_to_node(from_index)\n  to_node = manager.index_to_node(to_index)\n  data[:distance_matrix][from_node][to_node]\nend\n\ntransit_callback_index = routing.register_transit_callback(distance_callback)\nrouting.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)\n\n# run the solver\nassignment = routing.solve(first_solution_strategy: :path_cheapest_arc)\n\n# print the solution\nputs \"Objective: #{assignment.objective_value} miles\"\nindex = routing.start(0)\nplan_output = String.new(\"Route for vehicle 0:\\n\")\nroute_distance = 0\nwhile !routing.end?(index)\n  plan_output += \" #{manager.index_to_node(index)} -\u003e\"\n  previous_index = index\n  index = assignment.value(routing.next_var(index))\n  route_distance += routing.arc_cost_for_vehicle(previous_index, index, 0)\nend\nplan_output += \" #{manager.index_to_node(index)}\\n\"\nputs plan_output\n```\n\n### Vehicle Routing Problem (VRP)\n\n[Guide](https://developers.google.com/optimization/routing/vrp)\n\n```ruby\n# create the data\ndata = {}\ndata[:distance_matrix] = [\n  [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],\n  [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210],\n  [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754],\n  [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358],\n  [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244],\n  [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708],\n  [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480],\n  [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856],\n  [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514],\n  [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468],\n  [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354],\n  [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844],\n  [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730],\n  [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536],\n  [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194],\n  [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798],\n  [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0]\n]\ndata[:num_vehicles] = 4\ndata[:depot] = 0\n\n# define the distance callback\nmanager = ORTools::RoutingIndexManager.new(data[:distance_matrix].length, data[:num_vehicles], data[:depot])\nrouting = ORTools::RoutingModel.new(manager)\n\ndistance_callback = lambda do |from_index, to_index|\n  from_node = manager.index_to_node(from_index)\n  to_node = manager.index_to_node(to_index)\n  data[:distance_matrix][from_node][to_node]\nend\n\ntransit_callback_index = routing.register_transit_callback(distance_callback)\nrouting.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)\n\n# add a distance dimension\ndimension_name = \"Distance\"\nrouting.add_dimension(transit_callback_index, 0, 3000, true, dimension_name)\ndistance_dimension = routing.mutable_dimension(dimension_name)\ndistance_dimension.global_span_cost_coefficient = 100\n\n# run the solver\nsolution = routing.solve(first_solution_strategy: :path_cheapest_arc)\n\n# print the solution\nmax_route_distance = 0\ndata[:num_vehicles].times do |vehicle_id|\n  index = routing.start(vehicle_id)\n  plan_output = String.new(\"Route for vehicle #{vehicle_id}:\\n\")\n  route_distance = 0\n  while !routing.end?(index)\n    plan_output += \" #{manager.index_to_node(index)} -\u003e \"\n    previous_index = index\n    index = solution.value(routing.next_var(index))\n    route_distance += routing.arc_cost_for_vehicle(previous_index, index, vehicle_id)\n  end\n  plan_output += \"#{manager.index_to_node(index)}\\n\"\n  plan_output += \"Distance of the route: #{route_distance}m\\n\\n\"\n  puts plan_output\n  max_route_distance = [route_distance, max_route_distance].max\nend\nputs \"Maximum of the route distances: #{max_route_distance}m\"\n```\n\n### Capacity Constraints\n\n[Guide](https://developers.google.com/optimization/routing/cvrp)\n\n```ruby\ndata = {}\ndata[:distance_matrix] = [\n  [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],\n  [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210],\n  [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754],\n  [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358],\n  [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244],\n  [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708],\n  [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480],\n  [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856],\n  [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514],\n  [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468],\n  [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354],\n  [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844],\n  [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730],\n  [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536],\n  [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194],\n  [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798],\n  [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0]\n]\ndata[:demands] = [0, 1, 1, 2, 4, 2, 4, 8, 8, 1, 2, 1, 2, 4, 4, 8, 8]\ndata[:vehicle_capacities] = [15, 15, 15, 15]\ndata[:num_vehicles] = 4\ndata[:depot] = 0\n\nmanager = ORTools::RoutingIndexManager.new(data[:distance_matrix].size, data[:num_vehicles], data[:depot])\nrouting = ORTools::RoutingModel.new(manager)\n\ndistance_callback = lambda do |from_index, to_index|\n  from_node = manager.index_to_node(from_index)\n  to_node = manager.index_to_node(to_index)\n  data[:distance_matrix][from_node][to_node]\nend\n\ntransit_callback_index = routing.register_transit_callback(distance_callback)\n\nrouting.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)\n\ndemand_callback = lambda do |from_index|\n  from_node = manager.index_to_node(from_index)\n  data[:demands][from_node]\nend\n\ndemand_callback_index = routing.register_unary_transit_callback(demand_callback)\nrouting.add_dimension_with_vehicle_capacity(\n  demand_callback_index,\n  0,  # null capacity slack\n  data[:vehicle_capacities],  # vehicle maximum capacities\n  true,  # start cumul to zero\n  \"Capacity\"\n)\n\nsolution = routing.solve(first_solution_strategy: :path_cheapest_arc)\n\ntotal_distance = 0\ntotal_load = 0\ndata[:num_vehicles].times do |vehicle_id|\n  index = routing.start(vehicle_id)\n  plan_output = String.new(\"Route for vehicle #{vehicle_id}:\\n\")\n  route_distance = 0\n  route_load = 0\n  while !routing.end?(index)\n    node_index = manager.index_to_node(index)\n    route_load += data[:demands][node_index]\n    plan_output += \" #{node_index} Load(#{route_load}) -\u003e \"\n    previous_index = index\n    index = solution.value(routing.next_var(index))\n    route_distance += routing.arc_cost_for_vehicle(previous_index, index, vehicle_id)\n  end\n  plan_output += \" #{manager.index_to_node(index)} Load(#{route_load})\\n\"\n  plan_output += \"Distance of the route: #{route_distance}m\\n\"\n  plan_output += \"Load of the route: #{route_load}\\n\\n\"\n  puts plan_output\n  total_distance += route_distance\n  total_load += route_load\nend\nputs \"Total distance of all routes: #{total_distance}m\"\nputs \"Total load of all routes: #{total_load}\"\n```\n\n### Pickups and Deliveries\n\n[Guide](https://developers.google.com/optimization/routing/pickup_delivery)\n\n```ruby\ndata = {}\ndata[:distance_matrix] = [\n  [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],\n  [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210],\n  [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754],\n  [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358],\n  [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244],\n  [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708],\n  [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480],\n  [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856],\n  [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514],\n  [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468],\n  [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354],\n  [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844],\n  [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730],\n  [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536],\n  [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194],\n  [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798],\n  [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0]\n]\ndata[:pickups_deliveries] = [\n  [1, 6],\n  [2, 10],\n  [4, 3],\n  [5, 9],\n  [7, 8],\n  [15, 11],\n  [13, 12],\n  [16, 14],\n]\ndata[:num_vehicles] = 4\ndata[:depot] = 0\n\nmanager = ORTools::RoutingIndexManager.new(data[:distance_matrix].size, data[:num_vehicles], data[:depot])\nrouting = ORTools::RoutingModel.new(manager)\n\ndistance_callback = lambda do |from_index, to_index|\n  from_node = manager.index_to_node(from_index)\n  to_node = manager.index_to_node(to_index)\n  data[:distance_matrix][from_node][to_node]\nend\n\ntransit_callback_index = routing.register_transit_callback(distance_callback)\nrouting.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)\n\ndimension_name = \"Distance\"\nrouting.add_dimension(\n  transit_callback_index,\n  0,  # no slack\n  3000,  # vehicle maximum travel distance\n  true,  # start cumul to zero\n  dimension_name\n)\ndistance_dimension = routing.mutable_dimension(dimension_name)\ndistance_dimension.global_span_cost_coefficient = 100\n\ndata[:pickups_deliveries].each do |request|\n  pickup_index = manager.node_to_index(request[0])\n  delivery_index = manager.node_to_index(request[1])\n  routing.add_pickup_and_delivery(pickup_index, delivery_index)\n  routing.solver.add(routing.vehicle_var(pickup_index) == routing.vehicle_var(delivery_index))\n  routing.solver.add(distance_dimension.cumul_var(pickup_index) \u003c= distance_dimension.cumul_var(delivery_index))\nend\n\nsolution = routing.solve(first_solution_strategy: :parallel_cheapest_insertion)\n\ntotal_distance = 0\ndata[:num_vehicles].times do |vehicle_id|\n  index = routing.start(vehicle_id)\n  plan_output = String.new(\"Route for vehicle #{vehicle_id}:\\n\")\n  route_distance = 0\n  while !routing.end?(index)\n    plan_output += \" #{manager.index_to_node(index)} -\u003e \"\n    previous_index = index\n    index = solution.value(routing.next_var(index))\n    route_distance += routing.arc_cost_for_vehicle(previous_index, index, vehicle_id)\n  end\n  plan_output += \"#{manager.index_to_node(index)}\\n\"\n  plan_output += \"Distance of the route: #{route_distance}m\\n\\n\"\n  puts plan_output\n  total_distance += route_distance\nend\nputs \"Total Distance of all routes: #{total_distance}m\"\n```\n\n### Time Window Constraints\n\n[Guide](https://developers.google.com/optimization/routing/vrptw)\n\n```ruby\ndata = {}\ndata[:time_matrix] = [\n  [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7],\n  [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14],\n  [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9],\n  [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16],\n  [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14],\n  [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8],\n  [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5],\n  [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10],\n  [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6],\n  [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5],\n  [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4],\n  [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10],\n  [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8],\n  [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6],\n  [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2],\n  [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9],\n  [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0],\n]\ndata[:time_windows] = [\n  [0, 5],  # depot\n  [7, 12],  # 1\n  [10, 15],  # 2\n  [16, 18],  # 3\n  [10, 13],  # 4\n  [0, 5],  # 5\n  [5, 10],  # 6\n  [0, 4],  # 7\n  [5, 10],  # 8\n  [0, 3],  # 9\n  [10, 16],  # 10\n  [10, 15],  # 11\n  [0, 5],  # 12\n  [5, 10],  # 13\n  [7, 8],  # 14\n  [10, 15],  # 15\n  [11, 15],  # 16\n]\ndata[:num_vehicles] = 4\ndata[:depot] = 0\n\nmanager = ORTools::RoutingIndexManager.new(data[:time_matrix].size, data[:num_vehicles], data[:depot])\nrouting = ORTools::RoutingModel.new(manager)\n\ntime_callback = lambda do |from_index, to_index|\n  from_node = manager.index_to_node(from_index)\n  to_node = manager.index_to_node(to_index)\n  data[:time_matrix][from_node][to_node]\nend\n\ntransit_callback_index = routing.register_transit_callback(time_callback)\nrouting.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)\ntime = \"Time\"\nrouting.add_dimension(\n  transit_callback_index,\n  30,  # allow waiting time\n  30,  # maximum time per vehicle\n  false,  # don't force start cumul to zero\n  time\n)\ntime_dimension = routing.mutable_dimension(time)\n\ndata[:time_windows].each_with_index do |time_window, location_idx|\n  next if location_idx == 0\n  index = manager.node_to_index(location_idx)\n  time_dimension.cumul_var(index).set_range(time_window[0], time_window[1])\nend\n\ndata[:num_vehicles].times do |vehicle_id|\n  index = routing.start(vehicle_id)\n  time_dimension.cumul_var(index).set_range(data[:time_windows][0][0], data[:time_windows][0][1])\nend\n\ndata[:num_vehicles].times do |i|\n  routing.add_variable_minimized_by_finalizer(time_dimension.cumul_var(routing.start(i)))\n  routing.add_variable_minimized_by_finalizer(time_dimension.cumul_var(routing.end(i)))\nend\n\nsolution = routing.solve(first_solution_strategy: :path_cheapest_arc)\n\ntime_dimension = routing.mutable_dimension(\"Time\")\ntotal_time = 0\ndata[:num_vehicles].times do |vehicle_id|\n  index = routing.start(vehicle_id)\n  plan_output = String.new(\"Route for vehicle #{vehicle_id}:\\n\")\n  while !routing.end?(index)\n    time_var = time_dimension.cumul_var(index)\n    plan_output += \"#{manager.index_to_node(index)} Time(#{solution.min(time_var)},#{solution.max(time_var)}) -\u003e \"\n    index = solution.value(routing.next_var(index))\n  end\n  time_var = time_dimension.cumul_var(index)\n  plan_output += \"#{manager.index_to_node(index)} Time(#{solution.min(time_var)},#{solution.max(time_var)})\\n\"\n  plan_output += \"Time of the route: #{solution.min(time_var)}min\\n\\n\"\n  puts plan_output\n  total_time += solution.min(time_var)\nend\nputs \"Total time of all routes: #{total_time}min\"\n```\n\n### Resource Constraints\n\n[Guide](https://developers.google.com/optimization/routing/cvrptw_resources)\n\n```ruby\ndata = {}\ndata[:time_matrix] = [\n  [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7],\n  [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14],\n  [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9],\n  [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16],\n  [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14],\n  [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8],\n  [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5],\n  [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10],\n  [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6],\n  [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5],\n  [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4],\n  [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10],\n  [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8],\n  [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6],\n  [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2],\n  [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9],\n  [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0]\n]\ndata[:time_windows] = [\n  [0, 5],  # depot\n  [7, 12],  # 1\n  [10, 15],  # 2\n  [5, 14],  # 3\n  [5, 13],  # 4\n  [0, 5],  # 5\n  [5, 10],  # 6\n  [0, 10],  # 7\n  [5, 10],  # 8\n  [0, 5],  # 9\n  [10, 16],  # 10\n  [10, 15],  # 11\n  [0, 5],  # 12\n  [5, 10],  # 13\n  [7, 12],  # 14\n  [10, 15],  # 15\n  [5, 15],  # 16\n]\ndata[:num_vehicles] = 4\ndata[:vehicle_load_time] = 5\ndata[:vehicle_unload_time] = 5\ndata[:depot_capacity] = 2\ndata[:depot] = 0\n\nmanager = ORTools::RoutingIndexManager.new(data[:time_matrix].size, data[:num_vehicles], data[:depot])\nrouting = ORTools::RoutingModel.new(manager)\n\ntime_callback = lambda do |from_index, to_index|\n  from_node = manager.index_to_node(from_index)\n  to_node = manager.index_to_node(to_index)\n  data[:time_matrix][from_node][to_node]\nend\n\ntransit_callback_index = routing.register_transit_callback(time_callback)\n\nrouting.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)\n\ntime = \"Time\"\nrouting.add_dimension(\n  transit_callback_index,\n  60,  # allow waiting time\n  60,  # maximum time per vehicle\n  false,  # don't force start cumul to zero\n  time\n)\ntime_dimension = routing.mutable_dimension(time)\ndata[:time_windows].each_with_index do |time_window, location_idx|\n  next if location_idx == 0\n  index = manager.node_to_index(location_idx)\n  time_dimension.cumul_var(index).set_range(time_window[0], time_window[1])\nend\n\ndata[:num_vehicles].times do |vehicle_id|\n  index = routing.start(vehicle_id)\n  time_dimension.cumul_var(index).set_range(data[:time_windows][0][0], data[:time_windows][0][1])\nend\n\nsolver = routing.solver\nintervals = []\ndata[:num_vehicles].times do |i|\n  intervals \u003c\u003c solver.fixed_duration_interval_var(\n    time_dimension.cumul_var(routing.start(i)),\n    data[:vehicle_load_time],\n    \"depot_interval\"\n  )\n  intervals \u003c\u003c solver.fixed_duration_interval_var(\n    time_dimension.cumul_var(routing.end(i)),\n    data[:vehicle_unload_time],\n    \"depot_interval\"\n  )\nend\n\ndepot_usage = [1] * intervals.size\nsolver.add(solver.cumulative(intervals, depot_usage, data[:depot_capacity], \"depot\"))\n\ndata[:num_vehicles].times do |i|\n  routing.add_variable_minimized_by_finalizer(time_dimension.cumul_var(routing.start(i)))\n  routing.add_variable_minimized_by_finalizer(time_dimension.cumul_var(routing.end(i)))\nend\n\nsolution = routing.solve(first_solution_strategy: :path_cheapest_arc)\n\ntime_dimension = routing.mutable_dimension(\"Time\")\ntotal_time = 0\ndata[:num_vehicles].times do |vehicle_id|\n  index = routing.start(vehicle_id)\n  plan_output = String.new(\"Route for vehicle #{vehicle_id}:\\n\")\n  while !routing.end?(index)\n    time_var = time_dimension.cumul_var(index)\n    plan_output += \"#{manager.index_to_node(index)} Time(#{solution.min(time_var)},#{solution.max(time_var)}) -\u003e \"\n    index = solution.value(routing.next_var(index))\n  end\n  time_var = time_dimension.cumul_var(index)\n  plan_output += \"#{manager.index_to_node(index)} Time(#{solution.min(time_var)},#{solution.max(time_var)})\\n\"\n  plan_output += \"Time of the route: #{solution.min(time_var)}min\\n\\n\"\n  puts plan_output\n  total_time += solution.min(time_var)\nend\nputs \"Total time of all routes: #{total_time}min\"\n```\n\n### Penalties and Dropping Visits\n\n[Guide](https://developers.google.com/optimization/routing/penalties)\n\n```ruby\ndata = {}\ndata[:distance_matrix] = [\n  [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],\n  [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210],\n  [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754],\n  [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358],\n  [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244],\n  [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708],\n  [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480],\n  [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856],\n  [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514],\n  [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468],\n  [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354],\n  [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844],\n  [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730],\n  [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536],\n  [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194],\n  [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798],\n  [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0]\n]\ndata[:demands] = [0, 1, 1, 3, 6, 3, 6, 8, 8, 1, 2, 1, 2, 6, 6, 8, 8]\ndata[:vehicle_capacities] = [15, 15, 15, 15]\ndata[:num_vehicles] = 4\ndata[:depot] = 0\n\nmanager = ORTools::RoutingIndexManager.new(data[:distance_matrix].size, data[:num_vehicles], data[:depot])\nrouting = ORTools::RoutingModel.new(manager)\n\ndistance_callback = lambda do |from_index, to_index|\n  from_node = manager.index_to_node(from_index)\n  to_node = manager.index_to_node(to_index)\n  data[:distance_matrix][from_node][to_node]\nend\n\ntransit_callback_index = routing.register_transit_callback(distance_callback)\n\nrouting.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)\n\ndemand_callback = lambda do |from_index|\n  from_node = manager.index_to_node(from_index)\n  data[:demands][from_node]\nend\n\ndemand_callback_index = routing.register_unary_transit_callback(demand_callback)\nrouting.add_dimension_with_vehicle_capacity(\n  demand_callback_index,\n  0,  # null capacity slack\n  data[:vehicle_capacities],  # vehicle maximum capacities\n  true,  # start cumul to zero\n  \"Capacity\"\n)\n\npenalty = 1000\n1.upto(data[:distance_matrix].size - 1) do |node|\n  routing.add_disjunction([manager.node_to_index(node)], penalty)\nend\n\nassignment = routing.solve(first_solution_strategy: :path_cheapest_arc)\n\ndropped_nodes = String.new(\"Dropped nodes:\")\nrouting.size.times do |node|\n  next if routing.start?(node) || routing.end?(node)\n\n  if assignment.value(routing.next_var(node)) == node\n    dropped_nodes += \" #{manager.index_to_node(node)}\"\n  end\nend\nputs dropped_nodes\n\ntotal_distance = 0\ntotal_load = 0\ndata[:num_vehicles].times do |vehicle_id|\n  index = routing.start(vehicle_id)\n  plan_output = \"Route for vehicle #{vehicle_id}:\\n\"\n  route_distance = 0\n  route_load = 0\n  while !routing.end?(index)\n    node_index = manager.index_to_node(index)\n    route_load += data[:demands][node_index]\n    plan_output += \" #{node_index} Load(#{route_load}) -\u003e \"\n    previous_index = index\n    index = assignment.value(routing.next_var(index))\n    route_distance += routing.arc_cost_for_vehicle(previous_index, index, vehicle_id)\n  end\n  plan_output += \" #{manager.index_to_node(index)} Load(#{route_load})\\n\"\n  plan_output += \"Distance of the route: #{route_distance}m\\n\"\n  plan_output += \"Load of the route: #{route_load}\\n\\n\"\n  puts plan_output\n  total_distance += route_distance\n  total_load += route_load\nend\nputs \"Total Distance of all routes: #{total_distance}m\"\nputs \"Total Load of all routes: #{total_load}\"\n```\n\n### Routing Options\n\n[Guide](https://developers.google.com/optimization/routing/routing_options)\n\n```ruby\nrouting.solve(\n  solution_limit: 10,\n  time_limit: 10, # seconds,\n  lns_time_limit: 10, # seconds\n  first_solution_strategy: :path_cheapest_arc,\n  local_search_metaheuristic: :guided_local_search,\n  log_search: true\n)\n```\n\n## Bin Packing\n\n### The Knapsack Problem\n\n[Guide](https://developers.google.com/optimization/bin/knapsack)\n\n```ruby\n# create the data\nvalues = [\n  360, 83, 59, 130, 431, 67, 230, 52, 93, 125, 670, 892, 600, 38, 48, 147,\n  78, 256, 63, 17, 120, 164, 432, 35, 92, 110, 22, 42, 50, 323, 514, 28,\n  87, 73, 78, 15, 26, 78, 210, 36, 85, 189, 274, 43, 33, 10, 19, 389, 276,\n  312\n]\nweights = [[\n  7, 0, 30, 22, 80, 94, 11, 81, 70, 64, 59, 18, 0, 36, 3, 8, 15, 42, 9, 0,\n  42, 47, 52, 32, 26, 48, 55, 6, 29, 84, 2, 4, 18, 56, 7, 29, 93, 44, 71,\n  3, 86, 66, 31, 65, 0, 79, 20, 65, 52, 13\n]]\ncapacities = [850]\n\n# declare the solver\nsolver = ORTools::KnapsackSolver.new(:branch_and_bound, \"KnapsackExample\")\n\n# call the solver\nsolver.init(values, weights, capacities)\ncomputed_value = solver.solve\n\npacked_items = []\npacked_weights = []\ntotal_weight = 0\nputs \"Total value = #{computed_value}\"\nvalues.length.times do |i|\n  if solver.best_solution_contains?(i)\n    packed_items \u003c\u003c i\n    packed_weights \u003c\u003c weights[0][i]\n    total_weight += weights[0][i]\n  end\nend\nputs \"Total weight: #{total_weight}\"\nputs \"Packed items: #{packed_items}\"\nputs \"Packed weights: #{packed_weights}\"\n```\n\n### Multiple Knapsacks\n\n[Guide](https://developers.google.com/optimization/bin/multiple_knapsack)\n\n```ruby\n# create the data\ndata = {}\ndata[:weights] = [48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36]\ndata[:values] = [10, 30, 25, 50, 35, 30, 15, 40, 30, 35, 45, 10, 20, 30, 25]\ndata[:num_items] = data[:weights].length\ndata[:all_items] = data[:num_items].times.to_a\ndata[:bin_capacities] = [100, 100, 100, 100, 100]\ndata[:num_bins] = data[:bin_capacities].length\ndata[:all_bins] = data[:num_bins].times.to_a\n\n# declare the solver\nsolver = ORTools::Solver.new(\"CBC\")\n\n# create the variables\nx = {}\ndata[:all_items].each do |i|\n  data[:all_bins].each do |b|\n    x[[i, b]] = solver.bool_var(\"x_#{i}_#{b}\")\n  end\nend\n\n# each item is assigned to at most one bin\ndata[:all_items].each do |i|\n  solver.add(data[:all_bins].sum { |b| x[[i, b]] } \u003c= 1)\nend\n\n# the amount packed in each bin cannot exceed its capacity\ndata[:all_bins].each do |b|\n  solver.add(data[:all_items].sum { |i| x[[i, b]] * data[:weights][i] } \u003c= data[:bin_capacities][b])\nend\n\n# maximize total value of packed items\nobjective = solver.objective\ndata[:all_items].each do |i|\n  data[:all_bins].each do |b|\n    objective.set_coefficient(x[[i, b]], data[:values][i])\n  end\nend\nobjective.set_maximization\n\n# call the solver and print the solution\nstatus = solver.solve\n\nif status == :optimal\n  puts \"Total packed value: #{objective.value}\"\n  total_weight = 0\n  data[:all_bins].each do |b|\n    bin_weight = 0\n    bin_value = 0\n    puts \"Bin #{b}\\n\\n\"\n    data[:all_items].each do |i|\n      if x[[i, b]].solution_value \u003e 0\n        puts \"Item #{i} - weight: #{data[:weights][i]}  value: #{data[:values][i]}\"\n        bin_weight += data[:weights][i]\n        bin_value += data[:values][i]\n      end\n    end\n    puts \"Packed bin weight: #{bin_weight}\"\n    puts \"Packed bin value: #{bin_value}\"\n    puts\n    total_weight += bin_weight\n  end\n  puts \"Total packed weight: #{total_weight}\"\nelse\n  puts \"The problem does not have an optimal solution.\"\nend\n```\n\n### Bin Packing Problem\n\n[Guide](https://developers.google.com/optimization/bin/bin_packing)\n\n```ruby\n# create the data\ndata = {}\nweights = [48, 30, 19, 36, 36, 27, 42, 42, 36, 24, 30]\ndata[:weights] = weights\ndata[:items] = (0...weights.length).to_a\ndata[:bins] = data[:items]\ndata[:bin_capacity] = 100\n\n# create the mip solver with the CBC backend\nsolver = ORTools::Solver.new(\"CBC\")\n\n# variables\n# x[i, j] = 1 if item i is packed in bin j\nx = {}\ndata[:items].each do |i|\n  data[:bins].each do |j|\n    x[[i, j]] = solver.int_var(0, 1, \"x_%i_%i\" % [i, j])\n  end\nend\n\n# y[j] = 1 if bin j is used\ny = {}\ndata[:bins].each do |j|\n  y[j] = solver.int_var(0, 1, \"y[%i]\" % j)\nend\n\n# constraints\n# each item must be in exactly one bin\ndata[:items].each do |i|\n  solver.add(data[:bins].sum { |j| x[[i, j]] } == 1)\nend\n\n# the amount packed in each bin cannot exceed its capacity\ndata[:bins].each do |j|\n  sum = data[:items].sum { |i| x[[i, j]] * data[:weights][i] }\n  solver.add(sum \u003c= y[j] * data[:bin_capacity])\nend\n\n# objective: minimize the number of bins used\nsolver.minimize(data[:bins].sum { |j| y[j] })\n\n# call the solver and print the solution\nstatus = solver.solve\n\nif status == :optimal\n  num_bins = 0\n  data[:bins].each do |j|\n    if y[j].solution_value == 1\n      bin_items = []\n      bin_weight = 0\n      data[:items].each do |i|\n        if x[[i, j]].solution_value \u003e 0\n          bin_items \u003c\u003c i\n          bin_weight += data[:weights][i]\n        end\n      end\n      if bin_weight \u003e 0\n        num_bins += 1\n        puts \"Bin number #{j}\"\n        puts \"  Items packed: #{bin_items}\"\n        puts \"  Total weight: #{bin_weight}\"\n        puts\n      end\n    end\n  end\n  puts\n  puts \"Number of bins used: #{num_bins}\"\n  puts \"Time = #{solver.wall_time} milliseconds\"\nelse\n  puts \"The problem does not have an optimal solution.\"\nend\n```\n\n## Network Flows\n\n### Maximum Flows\n\n[Guide](https://developers.google.com/optimization/flow/maxflow)\n\n```ruby\n# define the data\nstart_nodes = [0, 0, 0, 1, 1, 2, 2, 3, 3]\nend_nodes = [1, 2, 3, 2, 4, 3, 4, 2, 4]\ncapacities = [20, 30, 10, 40, 30, 10, 20, 5, 20]\n\n# declare the solver and add the arcs\nmax_flow = ORTools::SimpleMaxFlow.new\n\nstart_nodes.length.times do |i|\n  max_flow.add_arc_with_capacity(start_nodes[i], end_nodes[i], capacities[i])\nend\n\n# invoke the solver and display the results\nif max_flow.solve(0, 4) == :optimal\n  puts \"Max flow: #{max_flow.optimal_flow}\"\n  puts\n  puts \"  Arc    Flow / Capacity\"\n  max_flow.num_arcs.times do |i|\n    puts \"%1s -\u003e %1s   %3s  / %3s\" % [\n      max_flow.tail(i),\n      max_flow.head(i),\n      max_flow.flow(i),\n      max_flow.capacity(i)\n    ]\n  end\n  puts \"Source side min-cut: #{max_flow.source_side_min_cut}\"\n  puts \"Sink side min-cut: #{max_flow.sink_side_min_cut}\"\nelse\n  puts \"There was an issue with the max flow input.\"\nend\n```\n\n### Minimum Cost Flows\n\n[Guide](https://developers.google.com/optimization/flow/mincostflow)\n\n```ruby\n# define the data\nstart_nodes = [ 0, 0,  1, 1,  1,  2, 2,  3, 4]\nend_nodes   = [ 1, 2,  2, 3,  4,  3, 4,  4, 2]\ncapacities  = [15, 8, 20, 4, 10, 15, 4, 20, 5]\nunit_costs  = [ 4, 4,  2, 2,  6,  1, 3,  2, 3]\nsupplies = [20, 0, 0, -5, -15]\n\n# declare the solver and add the arcs\nmin_cost_flow = ORTools::SimpleMinCostFlow.new\n\nstart_nodes.length.times do |i|\n  min_cost_flow.add_arc_with_capacity_and_unit_cost(\n    start_nodes[i], end_nodes[i], capacities[i], unit_costs[i]\n  )\nend\n\nsupplies.length.times do |i|\n  min_cost_flow.set_node_supply(i, supplies[i])\nend\n\n# invoke the solver and display the results\nif min_cost_flow.solve == :optimal\n  puts \"Minimum cost #{min_cost_flow.optimal_cost}\"\n  puts\n  puts \"  Arc    Flow / Capacity  Cost\"\n  min_cost_flow.num_arcs.times do |i|\n    cost = min_cost_flow.flow(i) * min_cost_flow.unit_cost(i)\n    puts \"%1s -\u003e %1s   %3s  / %3s       %3s\" % [\n      min_cost_flow.tail(i),\n      min_cost_flow.head(i),\n      min_cost_flow.flow(i),\n      min_cost_flow.capacity(i),\n      cost\n    ]\n  end\nelse\n  puts \"There was an issue with the min cost flow input.\"\nend\n```\n\n### Assignment as a Min Cost Flow Problem\n\n[Guide](https://developers.google.com/optimization/flow/assignment_min_cost_flow)\n\n```ruby\n# create the solver\nmin_cost_flow = ORTools::SimpleMinCostFlow.new\n\n# create the data\nstart_nodes = [0, 0, 0, 0] + [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4] + [5, 6, 7, 8]\nend_nodes = [1, 2, 3, 4] + [5, 6, 7, 8, 5, 6, 7, 8, 5, 6, 7, 8, 5, 6, 7, 8] + [9, 9, 9, 9]\ncapacities = [1, 1, 1, 1] + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + [1, 1, 1, 1]\ncosts = [0, 0, 0, 0] + [90, 76, 75, 70, 35, 85, 55, 65, 125, 95, 90, 105, 45, 110, 95, 115] + [0, 0, 0, 0]\nsupplies = [4, 0, 0, 0, 0, 0, 0, 0, 0, -4]\nsource = 0\nsink = 9\ntasks = 4\n\n# create the graph and constraints\nstart_nodes.length.times do |i|\n  min_cost_flow.add_arc_with_capacity_and_unit_cost(\n    start_nodes[i], end_nodes[i], capacities[i], costs[i]\n  )\nend\n\nsupplies.length.times do |i|\n  min_cost_flow.set_node_supply(i, supplies[i])\nend\n\n# invoke the solver\nif min_cost_flow.solve == :optimal\n  puts \"Total cost = #{min_cost_flow.optimal_cost}\"\n  puts\n  min_cost_flow.num_arcs.times do |arc|\n    if min_cost_flow.tail(arc) != source \u0026\u0026 min_cost_flow.head(arc) != sink\n      if min_cost_flow.flow(arc) \u003e 0\n        puts \"Worker %d assigned to task %d.  Cost = %d\" % [\n          min_cost_flow.tail(arc),\n          min_cost_flow.head(arc),\n          min_cost_flow.unit_cost(arc)\n        ]\n      end\n    end\n  end\nelse\n  puts \"There was an issue with the min cost flow input.\"\nend\n```\n\n## Scheduling\n\n### Employee Scheduling\n\n[Guide](https://developers.google.com/optimization/scheduling/employee_scheduling)\n\n```ruby\n# define the data\nnum_nurses = 4\nnum_shifts = 3\nnum_days = 3\nall_nurses = num_nurses.times.to_a\nall_shifts = num_shifts.times.to_a\nall_days = num_days.times.to_a\n\n# create the variables\nmodel = ORTools::CpModel.new\n\nshifts = {}\nall_nurses.each do |n|\n  all_days.each do |d|\n    all_shifts.each do |s|\n      shifts[[n, d, s]] = model.new_bool_var(\"shift_n%id%is%i\" % [n, d, s])\n    end\n  end\nend\n\n# assign nurses to shifts\nall_days.each do |d|\n  all_shifts.each do |s|\n    model.add(model.sum(all_nurses.map { |n| shifts[[n, d, s]] }) == 1)\n  end\nend\n\nall_nurses.each do |n|\n  all_days.each do |d|\n    model.add(model.sum(all_shifts.map { |s| shifts[[n, d, s]] }) \u003c= 1)\n  end\nend\n\n# assign shifts evenly\nmin_shifts_per_nurse = (num_shifts * num_days) / num_nurses\nmax_shifts_per_nurse = min_shifts_per_nurse + 1\nall_nurses.each do |n|\n  num_shifts_worked = model.sum(all_days.flat_map { |d| all_shifts.map { |s| shifts[[n, d, s]] } })\n  model.add(num_shifts_worked \u003e= min_shifts_per_nurse)\n  model.add(num_shifts_worked \u003c= max_shifts_per_nurse)\nend\n\n# create a printer\nclass NursesPartialSolutionPrinter \u003c ORTools::CpSolverSolutionCallback\n  attr_reader :solution_count\n\n  def initialize(shifts, num_nurses, num_days, num_shifts, sols)\n    super()\n    @shifts = shifts\n    @num_nurses = num_nurses\n    @num_days = num_days\n    @num_shifts = num_shifts\n    @solutions = sols\n    @solution_count = 0\n  end\n\n  def on_solution_callback\n    if @solutions.include?(@solution_count)\n      puts \"Solution #{@solution_count}\"\n      @num_days.times do |d|\n        puts \"Day #{d}\"\n        @num_nurses.times do |n|\n          working = false\n          @num_shifts.times do |s|\n            if value(@shifts[[n, d, s]])\n              working = true\n              puts \"  Nurse %i works shift %i\" % [n, s]\n            end\n          end\n          unless working\n            puts \"  Nurse #{n} does not work\"\n          end\n        end\n      end\n      puts\n    end\n    @solution_count += 1\n  end\nend\n\n# call the solver and display the results\nsolver = ORTools::CpSolver.new\na_few_solutions = 5.times.to_a\nsolution_printer = NursesPartialSolutionPrinter.new(\n  shifts, num_nurses, num_days, num_shifts, a_few_solutions\n)\nsolver.search_for_all_solutions(model, solution_printer)\n\nputs\nputs \"Statistics\"\nputs \"  - conflicts       : %i\" % solver.num_conflicts\nputs \"  - branches        : %i\" % solver.num_branches\nputs \"  - wall time       : %f s\" % solver.wall_time\nputs \"  - solutions found : %i\" % solution_printer.solution_count\n```\n\n### The Job Shop Problem\n\n[Guide](https://developers.google.com/optimization/scheduling/job_shop)\n\n```ruby\n# create the model\nmodel = ORTools::CpModel.new\n\njobs_data = [\n  [[0, 3], [1, 2], [2, 2]],\n  [[0, 2], [2, 1], [1, 4]],\n  [[1, 4], [2, 3]]\n]\n\nmachines_count = 1 + jobs_data.flat_map { |job| job.map { |task| task[0] }  }.max\nall_machines = machines_count.times.to_a\n\n# computes horizon dynamically as the sum of all durations\nhorizon = jobs_data.flat_map { |job| job.map { |task| task[1] }  }.sum\n\n# creates job intervals and add to the corresponding machine lists\nall_tasks = {}\nmachine_to_intervals = Hash.new { |hash, key| hash[key] = [] }\n\njobs_data.each_with_index do |job, job_id|\n  job.each_with_index do |task, task_id|\n    machine = task[0]\n    duration = task[1]\n    suffix = \"_%i_%i\" % [job_id, task_id]\n    start_var = model.new_int_var(0, horizon, \"start\" + suffix)\n    duration_var = model.new_int_var(duration, duration, \"duration\" + suffix)\n    end_var = model.new_int_var(0, horizon, \"end\" + suffix)\n    interval_var = model.new_interval_var(start_var, duration_var, end_var, \"interval\" + suffix)\n    all_tasks[[job_id, task_id]] = {start: start_var, end: end_var, interval: interval_var}\n    machine_to_intervals[machine] \u003c\u003c interval_var\n  end\nend\n\n# create and add disjunctive constraints\nall_machines.each do |machine|\n  model.add_no_overlap(machine_to_intervals[machine])\nend\n\n# precedences inside a job\njobs_data.each_with_index do |job, job_id|\n  (job.size - 1).times do |task_id|\n    model.add(all_tasks[[job_id, task_id + 1]][:start] \u003e= all_tasks[[job_id, task_id]][:end])\n  end\nend\n\n# makespan objective\nobj_var = model.new_int_var(0, horizon, \"makespan\")\nmodel.add_max_equality(obj_var, jobs_data.map.with_index { |job, job_id| all_tasks[[job_id, job.size - 1]][:end] })\nmodel.minimize(obj_var)\n\n# solve model\nsolver = ORTools::CpSolver.new\nstatus = solver.solve(model)\n\n# create one list of assigned tasks per machine\nassigned_jobs = Hash.new { |hash, key| hash[key] = [] }\njobs_data.each_with_index do |job, job_id|\n  job.each_with_index do |task, task_id|\n    machine = task[0]\n    assigned_jobs[machine] \u003c\u003c {\n      start: solver.value(all_tasks[[job_id, task_id]][:start]),\n      job: job_id,\n      index: task_id,\n      duration: task[1]\n    }\n  end\nend\n\n# create per machine output lines\noutput = String.new(\"\")\nall_machines.each do |machine|\n  # sort by starting time\n  assigned_jobs[machine].sort_by! { |v| v[:start] }\n  sol_line_tasks = \"Machine #{machine}: \"\n  sol_line = \"           \"\n\n  assigned_jobs[machine].each do |assigned_task|\n    name = \"job_%i_%i\" % [assigned_task[:job], assigned_task[:index]]\n    # add spaces to output to align columns\n    sol_line_tasks += \"%-10s\" % name\n    start = assigned_task[:start]\n    duration = assigned_task[:duration]\n    sol_tmp = \"[%i,%i]\" % [start, start + duration]\n    # add spaces to output to align columns\n    sol_line += \"%-10s\" % sol_tmp\n  end\n\n  sol_line += \"\\n\"\n  sol_line_tasks += \"\\n\"\n  output += sol_line_tasks\n  output += sol_line\nend\n\n# finally print the solution found\nputs \"Optimal Schedule Length: %i\" % solver.objective_value\nputs output\n```\n\n## Other Examples\n\n### Sudoku\n\n[Example](https://github.com/google/or-tools/blob/stable/examples/python/sudoku_sat.py)\n\n```ruby\n# create the model\nmodel = ORTools::CpModel.new\n\ncell_size = 3\nline_size = cell_size**2\nline = (0...line_size).to_a\ncell = (0...cell_size).to_a\n\ninitial_grid = [\n  [0, 6, 0, 0, 5, 0, 0, 2, 0],\n  [0, 0, 0, 3, 0, 0, 0, 9, 0],\n  [7, 0, 0, 6, 0, 0, 0, 1, 0],\n  [0, 0, 6, 0, 3, 0, 4, 0, 0],\n  [0, 0, 4, 0, 7, 0, 1, 0, 0],\n  [0, 0, 5, 0, 9, 0, 8, 0, 0],\n  [0, 4, 0, 0, 0, 1, 0, 0, 6],\n  [0, 3, 0, 0, 0, 8, 0, 0, 0],\n  [0, 2, 0, 0, 4, 0, 0, 5, 0]\n]\n\ngrid = {}\nline.each do |i|\n  line.each do |j|\n    grid[[i, j]] = model.new_int_var(1, line_size, \"grid %i %i\" % [i, j])\n  end\nend\n\n# all different on rows\nline.each do |i|\n  model.add_all_different(line.map { |j| grid[[i, j]] })\nend\n\n# all different on columns\nline.each do |j|\n  model.add_all_different(line.map { |i| grid[[i, j]] })\nend\n\n# all different on cells\ncell.each do |i|\n  cell.each do |j|\n    one_cell = []\n    cell.each do |di|\n      cell.each do |dj|\n        one_cell \u003c\u003c grid[[i * cell_size + di, j * cell_size + dj]]\n      end\n    end\n    model.add_all_different(one_cell)\n  end\nend\n\n# initial values\nline.each do |i|\n  line.each do |j|\n    if initial_grid[i][j] != 0\n      model.add(grid[[i, j]] == initial_grid[i][j])\n    end\n  end\nend\n\n# solve and print solution\nsolver = ORTools::CpSolver.new\nstatus = solver.solve(model)\nif status == :optimal\n  line.each do |i|\n    p line.map { |j| solver.value(grid[[i, j]]) }\n  end\nend\n```\n\n### Wedding Seating Chart\n\n[Example](https://github.com/google/or-tools/blob/stable/examples/python/wedding_optimal_chart_sat.py)\n\n```ruby\n# From\n# Meghan L. Bellows and J. D. Luc Peterson\n# \"Finding an optimal seating chart for a wedding\"\n# https://www.improbable.com/news/2012/Optimal-seating-chart.pdf\n# https://www.improbable.com/2012/02/12/finding-an-optimal-seating-chart-for-a-wedding\n#\n# Every year, millions of brides (not to mention their mothers, future\n# mothers-in-law, and occasionally grooms) struggle with one of the\n# most daunting tasks during the wedding-planning process: the\n# seating chart. The guest responses are in, banquet hall is booked,\n# menu choices have been made. You think the hard parts are over,\n# but you have yet to embark upon the biggest headache of them all.\n# In order to make this process easier, we present a mathematical\n# formulation that models the seating chart problem. This model can\n# be solved to find the optimal arrangement of guests at tables.\n# At the very least, it can provide a starting point and hopefully\n# minimize stress and arguments.\n#\n# Adapted from\n# https://github.com/google/or-tools/blob/stable/examples/python/wedding_optimal_chart_sat.py\n\n# Easy problem (from the paper)\n# num_tables = 2\n# table_capacity = 10\n# min_known_neighbors = 1\n\n# Slightly harder problem (also from the paper)\nnum_tables = 5\ntable_capacity = 4\nmin_known_neighbors = 1\n\n# Connection matrix: who knows who, and how strong\n# is the relation\nc = [\n  [1, 50, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],\n  [50, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],\n  [1, 1, 1, 50, 1, 1, 1, 1, 10, 0, 0, 0, 0, 0, 0, 0, 0],\n  [1, 1, 50, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],\n  [1, 1, 1, 1, 1, 50, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],\n  [1, 1, 1, 1, 50, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],\n  [1, 1, 1, 1, 1, 1, 1, 50, 1, 0, 0, 0, 0, 0, 0, 0, 0],\n  [1, 1, 1, 1, 1, 1, 50, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],\n  [1, 1, 10, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 50, 1, 1, 1, 1, 1, 1],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 1, 1, 1, 1, 1, 1, 1],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],\n  [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]\n]\n\n# Names of the guests. B: Bride side, G: Groom side\nnames = [\n  \"Deb (B)\", \"John (B)\", \"Martha (B)\", \"Travis (B)\", \"Allan (B)\",\n  \"Lois (B)\", \"Jayne (B)\", \"Brad (B)\", \"Abby (B)\", \"Mary Helen (G)\",\n  \"Lee (G)\", \"Annika (G)\", \"Carl (G)\", \"Colin (G)\", \"Shirley (G)\",\n  \"DeAnn (G)\", \"Lori (G)\"\n]\n\nnum_guests = c.size\n\nall_tables = num_tables.times.to_a\nall_guests = num_guests.times.to_a\n\n# create the cp model\nmodel = ORTools::CpModel.new\n\n# decision variables\nseats = {}\nall_tables.each do |t|\n  all_guests.each do |g|\n    seats[[t, g]] = model.new_bool_var(\"guest %i seats on table %i\" % [g, t])\n  end\nend\n\npairs = all_guests.combination(2)\n\ncolocated = {}\npairs.each do |g1, g2|\n  colocated[[g1, g2]] = model.new_bool_var(\"guest %i seats with guest %i\" % [g1, g2])\nend\n\nsame_table = {}\npairs.each do |g1, g2|\n  all_tables.each do |t|\n    same_table[[g1, g2, t]] = model.new_bool_var(\"guest %i seats with guest %i on table %i\" % [g1, g2, t])\n  end\nend\n\n# Objective\nmodel.maximize(model.sum((num_guests - 1).times.flat_map { |g1| (g1 + 1).upto(num_guests - 1).select { |g2| c[g1][g2] \u003e 0 }.map { |g2| colocated[[g1, g2]] * c[g1][g2] } }))\n\n#\n# Constraints\n#\n\n# Everybody seats at one table.\nall_guests.each do |g|\n  model.add(model.sum(all_tables.map { |t| seats[[t, g]] }) == 1)\nend\n\n# Tables have a max capacity.\nall_tables.each do |t|\n  model.add(model.sum(all_guests.map { |g| seats[[t, g]] }) \u003c= table_capacity)\nend\n\n# Link colocated with seats\npairs.each do |g1, g2|\n  all_tables.each do |t|\n    # Link same_table and seats.\n    model.add_bool_or([seats[[t, g1]].not, seats[[t, g2]].not, same_table[[g1, g2, t]]])\n    model.add_implication(same_table[[g1, g2, t]], seats[[t, g1]])\n    model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])\n  end\n\n  # Link colocated and same_table.\n  model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])\nend\n\n# Min known neighbors rule.\nall_guests.each do |g|\n  model.add(\n    model.sum(\n      (g + 1).upto(num_guests - 1).\n      select { |g2| c[g][g2] \u003e 0 }.\n      product(all_tables).\n      map { |g2, t| same_table[[g, g2, t]] }\n    ) +\n    model.sum(\n      g.times.\n      select { |g1| c[g1][g] \u003e 0 }.\n      product(all_tables).\n      map { |g1, t| same_table[[g1, g, t]] }\n    ) \u003e= min_known_neighbors\n  )\nend\n\n# Symmetry breaking. First guest seats on the first table.\nmodel.add(seats[[0, 0]] == 1)\n\n# Solve model\nsolver = ORTools::CpSolver.new\nsolution_printer = WeddingChartPrinter.new(seats, names, num_tables, num_guests)\nsolver.solve(model, solution_printer)\n\nputs \"Statistics\"\nputs \"  - conflicts    : %i\" % solver.num_conflicts\nputs \"  - branches     : %i\" % solver.num_branches\nputs \"  - wall time    : %f s\" % solver.wall_time\nputs \"  - num solutions: %i\" % solution_printer.num_solutions\n```\n\n### Set Partitioning\n\n[Example](https://pythonhosted.org/PuLP/CaseStudies/a_set_partitioning_problem.html)\n\n```ruby\n# A set partitioning model of a wedding seating problem\n# Authors: Stuart Mitchell 2009\n\nmax_tables = 5\nmax_table_size = 4\nguests = %w(A B C D E F G I J K L M N O P Q R)\n\n# Find the happiness of the table\n# by calculating the maximum distance between the letters\ndef happiness(table)\n  (table[0].ord - table[-1].ord).abs\nend\n\n# create list of all possible tables\npossible_tables = []\n(1..max_table_size).each do |i|\n  possible_tables += guests.combination(i).to_a\nend\n\nsolver = ORTools::Solver.new(\"CBC\")\n\n# create a binary variable to state that a table setting is used\nx = {}\npossible_tables.each do |table|\n  x[table] = solver.int_var(0, 1, \"table #{table.join(\", \")}\")\nend\n\nsolver.minimize(possible_tables.sum { |table| x[table] * happiness(table) })\n\n# specify the maximum number of tables\nsolver.add(x.values.sum \u003c= max_tables)\n\n# a guest must seated at one and only one table\nguests.each do |guest|\n  tables_with_guest = possible_tables.select { |table| table.include?(guest) }\n  solver.add(tables_with_guest.sum { |table| x[table] } == 1)\nend\n\nstatus = solver.solve\n\nputs \"The chosen tables are out of a total of %s:\" % possible_tables.size\npossible_tables.each do |table|\n  if x[table].solution_value == 1\n    p table\n  end\nend\n```\n\n## History\n\nView the [changelog](https://github.com/ankane/or-tools-ruby/blob/master/CHANGELOG.md)\n\n## Contributing\n\nEveryone is encouraged to help improve this project. Here are a few ways you can help:\n\n- [Report bugs](https://github.com/ankane/or-tools-ruby/issues)\n- Fix bugs and [submit pull requests](https://github.com/ankane/or-tools-ruby/pulls)\n- Write, clarify, or fix documentation\n- Suggest or add new features\n\nTo get started with development:\n\n```sh\ngit clone https://github.com/ankane/or-tools-ruby.git\ncd or-tools-ruby\nbundle install\nbundle exec rake compile\nbundle exec rake test\n```\n\nResources\n\n- [OR-Tools Reference](https://developers.google.com/optimization/reference)\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fankane%2For-tools-ruby","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fankane%2For-tools-ruby","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fankane%2For-tools-ruby/lists"}