or-tools 0.2.0 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4377b9acdd93be666c700e05fb56cf01e59f088cf149f345ab888ef018c82d04
4
- data.tar.gz: f32d95eb857cf479db372f4c0c27b18590bb9137aa72a9b6e478789830b7ea68
3
+ metadata.gz: dba74899e2a57b086643657e5093f12e67fd4fcdda7e22c2b533911f9991e43a
4
+ data.tar.gz: 963c0b75f55b4b772ead3a8402ec8bb4b90570c23de1c7dc3a437758e8949af2
5
5
  SHA512:
6
- metadata.gz: ab952c566d3f84c57cce0b843c27ad6e3fa9c06f4e85b273af2c82564403cd6bb88e8f8ae1d39c21f5732e4d3415729b8d4bd87418a34f0d02c588d816ab9d49
7
- data.tar.gz: 95a03e432680341c029e21ae2148385c1628a7df127ea8707991a47d087f37cd5ebb4288518f16b1cfd7c84c03a254c0cf8c26c5c8d574b949c3ed3901ca9cfe
6
+ metadata.gz: a5dc8662ec580c97a905817867346615e2fbb187d5b75d6b2b839a86e7385a20b2ed4f9f4c13ed536079c6141cdd0bf95bfad7ea6408d4eb81d8f0ed09e32a68
7
+ data.tar.gz: 195967d1e662ba05751ff31fbec3c96ebac519cd58a343ca551f9ba6c12304542b7f4127a37f88e99cda6b214d42089df7ed9b5a3134c0ec1eb978ea72a95be4
@@ -1,3 +1,29 @@
1
+ ## 0.3.4 (2021-01-14)
2
+
3
+ - Added support for time limit for `CpSolver`
4
+ - Added `add_dimension_with_vehicle_transits` and `status` methods to `RoutingModel`
5
+
6
+ ## 0.3.3 (2020-10-12)
7
+
8
+ - Added support for start and end points for routing
9
+
10
+ ## 0.3.2 (2020-08-04)
11
+
12
+ - Updated OR-Tools to 7.8
13
+ - Added binary installation for Ubuntu 20.04
14
+
15
+ ## 0.3.1 (2020-07-21)
16
+
17
+ - Reduced gem size
18
+
19
+ ## 0.3.0 (2020-07-21)
20
+
21
+ - Updated OR-Tools to 7.7
22
+ - Added `BasicScheduler` class
23
+ - Added `Seating` class
24
+ - Added `TSP` class
25
+ - Added `Sudoku` class
26
+
1
27
  ## 0.2.0 (2020-05-22)
2
28
 
3
29
  - No longer need to download the OR-Tools C++ library separately on Mac, Ubuntu 18.04, Ubuntu 16.04, Debian 10, and CentOS 8
data/NOTICE.txt CHANGED
@@ -1,4 +1,5 @@
1
- Copyright 2020 Andrew Kane
1
+ Copyright 2010-2018 Google LLC
2
+ Copyright 2020-2021 Andrew Kane
2
3
 
3
4
  Licensed under the Apache License, Version 2.0 (the "License");
4
5
  you may not use this file except in compliance with the License.
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [OR-Tools](https://github.com/google/or-tools) - operations research tools - for Ruby
4
4
 
5
- [![Build Status](https://travis-ci.org/ankane/or-tools.svg?branch=master)](https://travis-ci.org/ankane/or-tools)
5
+ [![Build Status](https://github.com/ankane/or-tools/workflows/build/badge.svg?branch=master)](https://github.com/ankane/or-tools/actions)
6
6
 
7
7
  ## Installation
8
8
 
@@ -14,26 +14,269 @@ gem 'or-tools'
14
14
 
15
15
  Installation can take a few minutes as OR-Tools downloads and builds.
16
16
 
17
- ## Getting Started
17
+ ## Higher Level Interfaces
18
+
19
+ - [Scheduling](#scheduling)
20
+ - [Seating](#seating)
21
+ - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
22
+ - [Sudoku](#sudoku)
23
+
24
+ ### Scheduling
25
+
26
+ Specify people and their availabililty
27
+
28
+ ```ruby
29
+ people = [
30
+ {
31
+ availability: [
32
+ {starts_at: Time.parse("2020-01-01 08:00:00"), ends_at: Time.parse("2020-01-01 16:00:00")},
33
+ {starts_at: Time.parse("2020-01-02 08:00:00"), ends_at: Time.parse("2020-01-02 16:00:00")}
34
+ ],
35
+ max_hours: 40 # optional, applies to entire scheduling period
36
+ },
37
+ {
38
+ availability: [
39
+ {starts_at: Time.parse("2020-01-01 08:00:00"), ends_at: Time.parse("2020-01-01 16:00:00")},
40
+ {starts_at: Time.parse("2020-01-03 08:00:00"), ends_at: Time.parse("2020-01-03 16:00:00")}
41
+ ],
42
+ max_hours: 20
43
+ }
44
+ ]
45
+ ```
46
+
47
+ Specify shifts
48
+
49
+ ```ruby
50
+ shifts = [
51
+ {starts_at: Time.parse("2020-01-01 08:00:00"), ends_at: Time.parse("2020-01-01 16:00:00")},
52
+ {starts_at: Time.parse("2020-01-02 08:00:00"), ends_at: Time.parse("2020-01-02 16:00:00")},
53
+ {starts_at: Time.parse("2020-01-03 08:00:00"), ends_at: Time.parse("2020-01-03 16:00:00")}
54
+ ]
55
+ ```
56
+
57
+ Run the scheduler
58
+
59
+ ```ruby
60
+ scheduler = ORTools::BasicScheduler.new(people: people, shifts: shifts)
61
+ ```
62
+
63
+ The scheduler maximizes the number of assigned hours. A person must be available for the entire shift to be considered for it.
64
+
65
+ Get assignments (returns indexes of people and shifts)
66
+
67
+ ```ruby
68
+ scheduler.assignments
69
+ # [
70
+ # {person: 2, shift: 0},
71
+ # {person: 0, shift: 1},
72
+ # {person: 1, shift: 2}
73
+ # ]
74
+ ```
75
+
76
+ Get assigned hours and total hours
77
+
78
+ ```ruby
79
+ scheduler.assigned_hours
80
+ scheduler.total_hours
81
+ ```
82
+
83
+ Feel free to create an issue if you have a scheduling use case that’s not covered.
84
+
85
+ ### Seating
86
+
87
+ Create a seating chart based on personal connections. Uses [this approach](https://www.improbable.com/news/2012/Optimal-seating-chart.pdf).
88
+
89
+ Specify connections
90
+
91
+ ```ruby
92
+ connections = [
93
+ {people: ["A", "B", "C"], weight: 2},
94
+ {people: ["C", "D", "E", "F"], weight: 1}
95
+ ]
96
+ ```
97
+
98
+ Use different weights to prioritize seating. For a wedding, it may look like:
99
+
100
+ ```ruby
101
+ connections = [
102
+ {people: knows_partner1, weight: 1},
103
+ {people: knows_partner2, weight: 1},
104
+ {people: relationship1, weight: 100},
105
+ {people: relationship2, weight: 100},
106
+ {people: relationship3, weight: 100},
107
+ {people: friend_group1, weight: 10},
108
+ {people: friend_group2, weight: 10},
109
+ # ...
110
+ ]
111
+ ```
112
+
113
+ If two people have multiple connections, weights are added.
114
+
115
+ Specify tables and their capacity
116
+
117
+ ```ruby
118
+ tables = [3, 3]
119
+ ```
120
+
121
+ Assign seats
122
+
123
+ ```ruby
124
+ seating = ORTools::Seating.new(connections: connections, tables: tables)
125
+ ```
126
+
127
+ Each person will have a connection with at least one other person at their table.
128
+
129
+ Get tables
130
+
131
+ ```ruby
132
+ seating.assigned_tables
133
+ ```
134
+
135
+ Get assignments by person
136
+
137
+ ```ruby
138
+ seating.assignments
139
+ ```
140
+
141
+ Get all connections for a person
142
+
143
+ ```ruby
144
+ seating.connections_for(person)
145
+ ```
146
+
147
+ Get connections for a person at their table
148
+
149
+ ```ruby
150
+ seating.connections_for(person, same_table: true)
151
+ ```
152
+
153
+ ### Traveling Salesperson Problem (TSP)
154
+
155
+ Create locations - the first location will be the starting and ending point
156
+
157
+ ```ruby
158
+ locations = [
159
+ {name: "Tokyo", latitude: 35.6762, longitude: 139.6503},
160
+ {name: "Delhi", latitude: 28.7041, longitude: 77.1025},
161
+ {name: "Shanghai", latitude: 31.2304, longitude: 121.4737},
162
+ {name: "São Paulo", latitude: -23.5505, longitude: -46.6333},
163
+ {name: "Mexico City", latitude: 19.4326, longitude: -99.1332},
164
+ {name: "Cairo", latitude: 30.0444, longitude: 31.2357},
165
+ {name: "Mumbai", latitude: 19.0760, longitude: 72.8777},
166
+ {name: "Beijing", latitude: 39.9042, longitude: 116.4074},
167
+ {name: "Dhaka", latitude: 23.8103, longitude: 90.4125},
168
+ {name: "Osaka", latitude: 34.6937, longitude: 135.5023},
169
+ {name: "New York City", latitude: 40.7128, longitude: -74.0060},
170
+ {name: "Karachi", latitude: 24.8607, longitude: 67.0011},
171
+ {name: "Buenos Aires", latitude: -34.6037, longitude: -58.3816}
172
+ ]
173
+ ```
174
+
175
+ Locations can have any fields - only `latitude` and `longitude` are required
176
+
177
+ Get route
178
+
179
+ ```ruby
180
+ tsp = ORTools::TSP.new(locations)
181
+ tsp.route # [{name: "Tokyo", ...}, {name: "Osaka", ...}, ...]
182
+ ```
183
+
184
+ Get distances between locations on route
185
+
186
+ ```ruby
187
+ tsp.distances # [392.441, 1362.926, 1067.31, ...]
188
+ ```
189
+
190
+ Distances are in kilometers - multiply by `0.6214` for miles
191
+
192
+ Get total distance
193
+
194
+ ```ruby
195
+ tsp.total_distance
196
+ ```
197
+
198
+ ### Sudoku
199
+
200
+ Create a puzzle with zeros in empty cells
201
+
202
+ ```ruby
203
+ grid = [
204
+ [0, 6, 0, 0, 5, 0, 0, 2, 0],
205
+ [0, 0, 0, 3, 0, 0, 0, 9, 0],
206
+ [7, 0, 0, 6, 0, 0, 0, 1, 0],
207
+ [0, 0, 6, 0, 3, 0, 4, 0, 0],
208
+ [0, 0, 4, 0, 7, 0, 1, 0, 0],
209
+ [0, 0, 5, 0, 9, 0, 8, 0, 0],
210
+ [0, 4, 0, 0, 0, 1, 0, 0, 6],
211
+ [0, 3, 0, 0, 0, 8, 0, 0, 0],
212
+ [0, 2, 0, 0, 4, 0, 0, 5, 0]
213
+ ]
214
+ sudoku = ORTools::Sudoku.new(grid)
215
+ sudoku.solution
216
+ ```
217
+
218
+ It can also solve more advanced puzzles like [The Miracle](https://www.youtube.com/watch?v=yKf9aUIxdb4)
219
+
220
+ ```ruby
221
+ grid = [
222
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
223
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
224
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
225
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
226
+ [0, 0, 1, 0, 0, 0, 0, 0, 0],
227
+ [0, 0, 0, 0, 0, 0, 2, 0, 0],
228
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
229
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
230
+ [0, 0, 0, 0, 0, 0, 0, 0, 0]
231
+ ]
232
+ sudoku = ORTools::Sudoku.new(grid, anti_knight: true, anti_king: true, non_consecutive: true)
233
+ sudoku.solution
234
+ ```
235
+
236
+ And [this 4-digit puzzle](https://www.youtube.com/watch?v=hAyZ9K2EBF0)
237
+
238
+ ```ruby
239
+ grid = [
240
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
241
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
242
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
243
+ [3, 8, 4, 0, 0, 0, 0, 0, 0],
244
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
245
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
246
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
247
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
248
+ [0, 0, 0, 0, 0, 0, 0, 0, 2]
249
+ ]
250
+ sudoku = ORTools::Sudoku.new(grid, x: true, anti_knight: true, magic_square: true)
251
+ sudoku.solution
252
+ ```
253
+
254
+ ## Guides
18
255
 
19
256
  Linear Optimization
20
257
 
21
258
  - [The Glop Linear Solver](#the-glop-linear-solver)
22
259
 
260
+ Integer Optimization
261
+
262
+ - [Mixed-Integer Programming](#mixed-integer-programming)
263
+
23
264
  Constraint Optimization
24
265
 
25
266
  - [CP-SAT Solver](#cp-sat-solver)
26
267
  - [Solving an Optimization Problem](#solving-an-optimization-problem)
27
268
  - [Cryptarithmetic](#cryptarithmetic)
28
269
  - [The N-queens Problem](#the-n-queens-problem)
270
+ - [Setting Solver Limits](#setting-solver-limits) [master]
29
271
 
30
- Integer Optimization
272
+ Assignment
31
273
 
32
- - [Mixed-Integer Programming](#mixed-integer-programming)
274
+ - [Assignment](#assignment)
275
+ - [Assignment with Teams](#assignment-with-teams)
33
276
 
34
277
  Routing
35
278
 
36
- - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
279
+ - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp-1)
37
280
  - [Vehicle Routing Problem (VRP)](#vehicle-routing-problem-vrp)
38
281
  - [Capacity Constraints](#capacity-constraints)
39
282
  - [Pickups and Deliveries](#pickups-and-deliveries)
@@ -52,12 +295,7 @@ Network Flows
52
295
 
53
296
  - [Maximum Flows](#maximum-flows)
54
297
  - [Minimum Cost Flows](#minimum-cost-flows)
55
-
56
- Assignment
57
-
58
- - [Assignment](#assignment)
59
- - [Assignment as a Min Cost Problem](#assignment-as-a-min-cost-problem)
60
- - [Assignment as a MIP Problem](#assignment-as-a-mip-problem)
298
+ - [Assignment as a Min Cost Flow Problem](#assignment-as-a-min-cost-flow-problem)
61
299
 
62
300
  Scheduling
63
301
 
@@ -66,7 +304,7 @@ Scheduling
66
304
 
67
305
  Other Examples
68
306
 
69
- - [Sudoku](#sudoku)
307
+ - [Sudoku](#sudoku-1)
70
308
  - [Wedding Seating Chart](#wedding-seating-chart)
71
309
  - [Set Partitioning](#set-partitioning)
72
310
 
@@ -114,6 +352,52 @@ puts "y = #{y.solution_value}"
114
352
  puts "Optimal objective value = #{opt_solution}"
115
353
  ```
116
354
 
355
+ ### Mixed-Integer Programming
356
+
357
+ [Guide](https://developers.google.com/optimization/mip/integer_opt)
358
+
359
+ ```ruby
360
+ # declare the MIP solver
361
+ solver = ORTools::Solver.new("simple_mip_program", :cbc)
362
+
363
+ # define the variables
364
+ infinity = solver.infinity
365
+ x = solver.int_var(0.0, infinity, "x")
366
+ y = solver.int_var(0.0, infinity, "y")
367
+
368
+ puts "Number of variables = #{solver.num_variables}"
369
+
370
+ # define the constraints
371
+ c0 = solver.constraint(-infinity, 17.5)
372
+ c0.set_coefficient(x, 1)
373
+ c0.set_coefficient(y, 7)
374
+
375
+ c1 = solver.constraint(-infinity, 3.5)
376
+ c1.set_coefficient(x, 1);
377
+ c1.set_coefficient(y, 0);
378
+
379
+ puts "Number of constraints = #{solver.num_constraints}"
380
+
381
+ # define the objective
382
+ objective = solver.objective
383
+ objective.set_coefficient(x, 1)
384
+ objective.set_coefficient(y, 10)
385
+ objective.set_maximization
386
+
387
+ # call the solver
388
+ status = solver.solve
389
+
390
+ # display the solution
391
+ if status == :optimal
392
+ puts "Solution:"
393
+ puts "Objective value = #{solver.objective.value}"
394
+ puts "x = #{x.solution_value}"
395
+ puts "y = #{y.solution_value}"
396
+ else
397
+ puts "The problem does not have an optimal solution."
398
+ end
399
+ ```
400
+
117
401
  ### CP-SAT Solver
118
402
 
119
403
  [Guide](https://developers.google.com/optimization/cp/cp_solver)
@@ -136,7 +420,7 @@ solver = ORTools::CpSolver.new
136
420
  status = solver.solve(model)
137
421
 
138
422
  # display the first solution
139
- if status == :feasible
423
+ if status == :optimal
140
424
  puts "x = #{solver.value(x)}"
141
425
  puts "y = #{solver.value(y)}"
142
426
  puts "z = #{solver.value(z)}"
@@ -295,50 +579,152 @@ puts
295
579
  puts "Solutions found : %i" % solution_printer.solution_count
296
580
  ```
297
581
 
298
- ### Mixed-Integer Programming
582
+ ### Setting Solver Limits
299
583
 
300
- [Guide](https://developers.google.com/optimization/mip/integer_opt)
584
+ [Guide](https://developers.google.com/optimization/cp/cp_tasks)
301
585
 
302
586
  ```ruby
303
- # declare the MIP solver
304
- solver = ORTools::Solver.new("simple_mip_program", :cbc)
587
+ # create the model
588
+ model = ORTools::CpModel.new
305
589
 
306
- # define the variables
307
- infinity = solver.infinity
308
- x = solver.int_var(0.0, infinity, "x")
309
- y = solver.int_var(0.0, infinity, "y")
590
+ # create the variables
591
+ num_vals = 3
592
+ x = model.new_int_var(0, num_vals - 1, "x")
593
+ y = model.new_int_var(0, num_vals - 1, "y")
594
+ z = model.new_int_var(0, num_vals - 1, "z")
310
595
 
311
- puts "Number of variables = #{solver.num_variables}"
596
+ # add an all-different constraint
597
+ model.add(x != y)
312
598
 
313
- # define the constraints
314
- c0 = solver.constraint(-infinity, 17.5)
315
- c0.set_coefficient(x, 1)
316
- c0.set_coefficient(y, 7)
599
+ # create the solver
600
+ solver = ORTools::CpSolver.new
317
601
 
318
- c1 = solver.constraint(-infinity, 3.5)
319
- c1.set_coefficient(x, 1);
320
- c1.set_coefficient(y, 0);
602
+ # set a time limit of 10 seconds.
603
+ solver.parameters.max_time_in_seconds = 10
321
604
 
322
- puts "Number of constraints = #{solver.num_constraints}"
605
+ # solve the model
606
+ status = solver.solve(model)
323
607
 
324
- # define the objective
325
- objective = solver.objective
326
- objective.set_coefficient(x, 1)
327
- objective.set_coefficient(y, 10)
328
- objective.set_maximization
608
+ # display the first solution
609
+ if status == :optimal
610
+ puts "x = #{solver.value(x)}"
611
+ puts "y = #{solver.value(y)}"
612
+ puts "z = #{solver.value(z)}"
613
+ end
614
+ ```
329
615
 
330
- # call the solver
331
- status = solver.solve
616
+ ### Assignment
332
617
 
333
- # display the solution
334
- if status == :optimal
335
- puts "Solution:"
336
- puts "Objective value = #{solver.objective.value}"
337
- puts "x = #{x.solution_value}"
338
- puts "y = #{y.solution_value}"
339
- else
340
- puts "The problem does not have an optimal solution."
618
+ [Guide](https://developers.google.com/optimization/assignment/assignment_example)
619
+
620
+ ```ruby
621
+ # create the data
622
+ cost = [[ 90, 76, 75, 70],
623
+ [ 35, 85, 55, 65],
624
+ [125, 95, 90, 105],
625
+ [ 45, 110, 95, 115]]
626
+
627
+ rows = cost.length
628
+ cols = cost[0].length
629
+
630
+ # create the solver
631
+ assignment = ORTools::LinearSumAssignment.new
632
+
633
+ # add the costs to the solver
634
+ rows.times do |worker|
635
+ cols.times do |task|
636
+ if cost[worker][task]
637
+ assignment.add_arc_with_cost(worker, task, cost[worker][task])
638
+ end
639
+ end
640
+ end
641
+
642
+ # invoke the solver
643
+ solve_status = assignment.solve
644
+ if solve_status == :optimal
645
+ puts "Total cost = #{assignment.optimal_cost}"
646
+ puts
647
+ assignment.num_nodes.times do |i|
648
+ puts "Worker %d assigned to task %d. Cost = %d" % [
649
+ i,
650
+ assignment.right_mate(i),
651
+ assignment.assignment_cost(i)
652
+ ]
653
+ end
654
+ elsif solve_status == :infeasible
655
+ puts "No assignment is possible."
656
+ elsif solve_status == :possible_overflow
657
+ puts "Some input costs are too large and may cause an integer overflow."
658
+ end
659
+ ```
660
+
661
+ ### Assignment with Teams
662
+
663
+ [Guide](https://developers.google.com/optimization/assignment/assignment_teams)
664
+
665
+ ```ruby
666
+ # create the solver
667
+ solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
668
+
669
+ # create the data
670
+ cost = [[90, 76, 75, 70],
671
+ [35, 85, 55, 65],
672
+ [125, 95, 90, 105],
673
+ [45, 110, 95, 115],
674
+ [60, 105, 80, 75],
675
+ [45, 65, 110, 95]]
676
+
677
+ team1 = [0, 2, 4]
678
+ team2 = [1, 3, 5]
679
+ team_max = 2
680
+
681
+ # create the variables
682
+ num_workers = cost.length
683
+ num_tasks = cost[1].length
684
+ x = {}
685
+
686
+ num_workers.times do |i|
687
+ num_tasks.times do |j|
688
+ x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
689
+ end
341
690
  end
691
+
692
+ # create the objective function
693
+ solver.minimize(solver.sum(
694
+ num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
695
+ ))
696
+
697
+ # create the constraints
698
+ num_workers.times do |i|
699
+ solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
700
+ end
701
+
702
+ num_tasks.times do |j|
703
+ solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
704
+ end
705
+
706
+ solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
707
+ solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
708
+
709
+ # invoke the solver
710
+ sol = solver.solve
711
+
712
+ puts "Total cost = #{solver.objective.value}"
713
+ puts
714
+ num_workers.times do |i|
715
+ num_tasks.times do |j|
716
+ if x[[i, j]].solution_value > 0
717
+ puts "Worker %d assigned to task %d. Cost = %d" % [
718
+ i,
719
+ j,
720
+ cost[i][j]
721
+ ]
722
+ end
723
+ end
724
+ end
725
+
726
+ puts
727
+ puts "Time = #{solver.wall_time} milliseconds"
342
728
  ```
343
729
 
344
730
  ### Traveling Salesperson Problem (TSP)
@@ -380,7 +766,7 @@ transit_callback_index = routing.register_transit_callback(distance_callback)
380
766
  routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
381
767
 
382
768
  # run the solver
383
- assignment = routing.solve(first_solution_strategy: :path_cheaper_arc)
769
+ assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
384
770
 
385
771
  # print the solution
386
772
  puts "Objective: #{assignment.objective_value} miles"
@@ -1276,52 +1662,7 @@ else
1276
1662
  end
1277
1663
  ```
1278
1664
 
1279
- ### Assignment
1280
-
1281
- [Guide](https://developers.google.com/optimization/assignment/simple_assignment)
1282
-
1283
- ```ruby
1284
- # create the data
1285
- cost = [[ 90, 76, 75, 70],
1286
- [ 35, 85, 55, 65],
1287
- [125, 95, 90, 105],
1288
- [ 45, 110, 95, 115]]
1289
-
1290
- rows = cost.length
1291
- cols = cost[0].length
1292
-
1293
- # create the solver
1294
- assignment = ORTools::LinearSumAssignment.new
1295
-
1296
- # add the costs to the solver
1297
- rows.times do |worker|
1298
- cols.times do |task|
1299
- if cost[worker][task]
1300
- assignment.add_arc_with_cost(worker, task, cost[worker][task])
1301
- end
1302
- end
1303
- end
1304
-
1305
- # invoke the solver
1306
- solve_status = assignment.solve
1307
- if solve_status == :optimal
1308
- puts "Total cost = #{assignment.optimal_cost}"
1309
- puts
1310
- assignment.num_nodes.times do |i|
1311
- puts "Worker %d assigned to task %d. Cost = %d" % [
1312
- i,
1313
- assignment.right_mate(i),
1314
- assignment.assignment_cost(i)
1315
- ]
1316
- end
1317
- elsif solve_status == :infeasible
1318
- puts "No assignment is possible."
1319
- elsif solve_status == :possible_overflow
1320
- puts "Some input costs are too large and may cause an integer overflow."
1321
- end
1322
- ```
1323
-
1324
- ### Assignment as a Min Cost Problem
1665
+ ### Assignment as a Min Cost Flow Problem
1325
1666
 
1326
1667
  [Guide](https://developers.google.com/optimization/assignment/assignment_min_cost_flow)
1327
1668
 
@@ -1370,75 +1711,6 @@ else
1370
1711
  end
1371
1712
  ```
1372
1713
 
1373
- ### Assignment as a MIP Problem
1374
-
1375
- [Guide](https://developers.google.com/optimization/assignment/assignment_mip)
1376
-
1377
- ```ruby
1378
- # create the solver
1379
- solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
1380
-
1381
- # create the data
1382
- cost = [[90, 76, 75, 70],
1383
- [35, 85, 55, 65],
1384
- [125, 95, 90, 105],
1385
- [45, 110, 95, 115],
1386
- [60, 105, 80, 75],
1387
- [45, 65, 110, 95]]
1388
-
1389
- team1 = [0, 2, 4]
1390
- team2 = [1, 3, 5]
1391
- team_max = 2
1392
-
1393
- # create the variables
1394
- num_workers = cost.length
1395
- num_tasks = cost[1].length
1396
- x = {}
1397
-
1398
- num_workers.times do |i|
1399
- num_tasks.times do |j|
1400
- x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
1401
- end
1402
- end
1403
-
1404
- # create the objective function
1405
- solver.minimize(solver.sum(
1406
- num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
1407
- ))
1408
-
1409
- # create the constraints
1410
- num_workers.times do |i|
1411
- solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
1412
- end
1413
-
1414
- num_tasks.times do |j|
1415
- solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
1416
- end
1417
-
1418
- solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
1419
- solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
1420
-
1421
- # invoke the solver
1422
- sol = solver.solve
1423
-
1424
- puts "Total cost = #{solver.objective.value}"
1425
- puts
1426
- num_workers.times do |i|
1427
- num_tasks.times do |j|
1428
- if x[[i, j]].solution_value > 0
1429
- puts "Worker %d assigned to task %d. Cost = %d" % [
1430
- i,
1431
- j,
1432
- cost[i][j]
1433
- ]
1434
- end
1435
- end
1436
- end
1437
-
1438
- puts
1439
- puts "Time = #{solver.wall_time} milliseconds"
1440
- ```
1441
-
1442
1714
  ### Employee Scheduling
1443
1715
 
1444
1716
  [Guide](https://developers.google.com/optimization/scheduling/employee_scheduling)
@@ -1710,7 +1982,7 @@ end
1710
1982
  # solve and print solution
1711
1983
  solver = ORTools::CpSolver.new
1712
1984
  status = solver.solve(model)
1713
- if status == :feasible
1985
+ if status == :optimal
1714
1986
  line.each do |i|
1715
1987
  p line.map { |j| solver.value(grid[[i, j]]) }
1716
1988
  end
@@ -1725,8 +1997,8 @@ end
1725
1997
  # From
1726
1998
  # Meghan L. Bellows and J. D. Luc Peterson
1727
1999
  # "Finding an optimal seating chart for a wedding"
1728
- # http://www.improbable.com/news/2012/Optimal-seating-chart.pdf
1729
- # http://www.improbable.com/2012/02/12/finding-an-optimal-seating-chart-for-a-wedding
2000
+ # https://www.improbable.com/news/2012/Optimal-seating-chart.pdf
2001
+ # https://www.improbable.com/2012/02/12/finding-an-optimal-seating-chart-for-a-wedding
1730
2002
  #
1731
2003
  # Every year, millions of brides (not to mention their mothers, future
1732
2004
  # mothers-in-law, and occasionally grooms) struggle with one of the
@@ -1799,19 +2071,17 @@ all_tables.each do |t|
1799
2071
  end
1800
2072
  end
1801
2073
 
2074
+ pairs = all_guests.combination(2)
2075
+
1802
2076
  colocated = {}
1803
- (num_guests - 1).times do |g1|
1804
- (g1 + 1).upto(num_guests - 1) do |g2|
1805
- colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
1806
- end
2077
+ pairs.each do |g1, g2|
2078
+ colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
1807
2079
  end
1808
2080
 
1809
2081
  same_table = {}
1810
- (num_guests - 1).times do |g1|
1811
- (g1 + 1).upto(num_guests - 1) do |g2|
1812
- all_tables.each do |t|
1813
- same_table[[g1, g2, t]] = model.new_bool_var("guest %i seats with guest %i on table %i" % [g1, g2, t])
1814
- end
2082
+ pairs.each do |g1, g2|
2083
+ all_tables.each do |t|
2084
+ same_table[[g1, g2, t]] = model.new_bool_var("guest %i seats with guest %i on table %i" % [g1, g2, t])
1815
2085
  end
1816
2086
  end
1817
2087
 
@@ -1833,29 +2103,32 @@ all_tables.each do |t|
1833
2103
  end
1834
2104
 
1835
2105
  # Link colocated with seats
1836
- (num_guests - 1).times do |g1|
1837
- (g1 + 1).upto(num_guests - 1) do |g2|
1838
- all_tables.each do |t|
1839
- # Link same_table and seats.
1840
- model.add_bool_or([seats[[t, g1]].not, seats[[t, g2]].not, same_table[[g1, g2, t]]])
1841
- model.add_implication(same_table[[g1, g2, t]], seats[[t, g1]])
1842
- model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])
1843
- end
1844
-
1845
- # Link colocated and same_table.
1846
- model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])
2106
+ pairs.each do |g1, g2|
2107
+ all_tables.each do |t|
2108
+ # Link same_table and seats.
2109
+ model.add_bool_or([seats[[t, g1]].not, seats[[t, g2]].not, same_table[[g1, g2, t]]])
2110
+ model.add_implication(same_table[[g1, g2, t]], seats[[t, g1]])
2111
+ model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])
1847
2112
  end
2113
+
2114
+ # Link colocated and same_table.
2115
+ model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])
1848
2116
  end
1849
2117
 
1850
2118
  # Min known neighbors rule.
1851
- all_tables.each do |t|
2119
+ all_guests.each do |g|
1852
2120
  model.add(
1853
2121
  model.sum(
1854
- (num_guests - 1).times.flat_map { |g1|
1855
- (g1 + 1).upto(num_guests - 1).select { |g2| c[g1][g2] > 0 }.flat_map { |g2|
1856
- all_tables.map { |t2| same_table[[g1, g2, t2]] }
1857
- }
1858
- }
2122
+ (g + 1).upto(num_guests - 1).
2123
+ select { |g2| c[g][g2] > 0 }.
2124
+ product(all_tables).
2125
+ map { |g2, t| same_table[[g, g2, t]] }
2126
+ ) +
2127
+ model.sum(
2128
+ g.times.
2129
+ select { |g1| c[g1][g] > 0 }.
2130
+ product(all_tables).
2131
+ map { |g1, t| same_table[[g1, g, t]] }
1859
2132
  ) >= min_known_neighbors
1860
2133
  )
1861
2134
  end