or-tools 0.2.0 → 0.3.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/NOTICE.txt +2 -1
- data/README.md +463 -190
- data/ext/or-tools/ext.cpp +64 -6
- data/ext/or-tools/vendor.rb +11 -8
- data/lib/or-tools.rb +7 -0
- data/lib/or_tools/basic_scheduler.rb +86 -0
- data/lib/or_tools/cp_solver.rb +12 -4
- data/lib/or_tools/routing_index_manager.rb +11 -0
- data/lib/or_tools/seating.rb +115 -0
- data/lib/or_tools/sudoku.rb +132 -0
- data/lib/or_tools/tsp.rb +60 -0
- data/lib/or_tools/version.rb +1 -1
- metadata +12 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dba74899e2a57b086643657e5093f12e67fd4fcdda7e22c2b533911f9991e43a
|
4
|
+
data.tar.gz: 963c0b75f55b4b772ead3a8402ec8bb4b90570c23de1c7dc3a437758e8949af2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a5dc8662ec580c97a905817867346615e2fbb187d5b75d6b2b839a86e7385a20b2ed4f9f4c13ed536079c6141cdd0bf95bfad7ea6408d4eb81d8f0ed09e32a68
|
7
|
+
data.tar.gz: 195967d1e662ba05751ff31fbec3c96ebac519cd58a343ca551f9ba6c12304542b7f4127a37f88e99cda6b214d42089df7ed9b5a3134c0ec1eb978ea72a95be4
|
data/CHANGELOG.md
CHANGED
@@ -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
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://
|
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
|
-
##
|
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
|
-
|
272
|
+
Assignment
|
31
273
|
|
32
|
-
- [
|
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 == :
|
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
|
-
###
|
582
|
+
### Setting Solver Limits
|
299
583
|
|
300
|
-
[Guide](https://developers.google.com/optimization/
|
584
|
+
[Guide](https://developers.google.com/optimization/cp/cp_tasks)
|
301
585
|
|
302
586
|
```ruby
|
303
|
-
#
|
304
|
-
|
587
|
+
# create the model
|
588
|
+
model = ORTools::CpModel.new
|
305
589
|
|
306
|
-
#
|
307
|
-
|
308
|
-
x =
|
309
|
-
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
|
-
|
596
|
+
# add an all-different constraint
|
597
|
+
model.add(x != y)
|
312
598
|
|
313
|
-
#
|
314
|
-
|
315
|
-
c0.set_coefficient(x, 1)
|
316
|
-
c0.set_coefficient(y, 7)
|
599
|
+
# create the solver
|
600
|
+
solver = ORTools::CpSolver.new
|
317
601
|
|
318
|
-
|
319
|
-
|
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
|
-
|
605
|
+
# solve the model
|
606
|
+
status = solver.solve(model)
|
323
607
|
|
324
|
-
#
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
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
|
-
|
331
|
-
status = solver.solve
|
616
|
+
### Assignment
|
332
617
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
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: :
|
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 == :
|
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
|
-
#
|
1729
|
-
#
|
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
|
-
|
1804
|
-
|
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
|
-
|
1811
|
-
|
1812
|
-
|
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
|
-
|
1837
|
-
|
1838
|
-
|
1839
|
-
|
1840
|
-
|
1841
|
-
|
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
|
-
|
2119
|
+
all_guests.each do |g|
|
1852
2120
|
model.add(
|
1853
2121
|
model.sum(
|
1854
|
-
(num_guests - 1).
|
1855
|
-
|
1856
|
-
|
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
|