or-tools 0.1.4 → 0.3.2
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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +430 -195
- data/ext/or-tools/ext.cpp +5 -0
- data/ext/or-tools/extconf.rb +21 -63
- data/ext/or-tools/vendor.rb +98 -0
- data/lib/or-tools.rb +6 -0
- data/lib/or_tools/basic_scheduler.rb +86 -0
- data/lib/or_tools/cp_solver.rb +5 -1
- 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 +7 -3
- data/lib/or_tools/ext.bundle +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae2a270bed20b9d1236296f7368506b7ddb2172470ab2d57c655340f2c3a7a13
|
4
|
+
data.tar.gz: ef2e7101057762004b6bbb4f72b3dd5da50bc3cbfa236673f87b4b55d8aff901
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 534ac8c95ae51699667d0300f2d61664a28c8f992392a6621e70b47bae500fab41a9c30321a8a57f90a58abc5a740cb31c796e7f0447fa3d191161dd764f7467
|
7
|
+
data.tar.gz: 2418aec568f5ae54883dd2882ea961e79ace65d889dd1442cadfd11a2782fae965f8e18298e851794ac4e0a0e3b2f8c82ff3febc38e112016a2cb2236febf656
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,28 @@
|
|
1
|
+
## 0.3.2 (2020-08-04)
|
2
|
+
|
3
|
+
- Updated OR-Tools to 7.8
|
4
|
+
- Added binary installation for Ubuntu 20.04
|
5
|
+
|
6
|
+
## 0.3.1 (2020-07-21)
|
7
|
+
|
8
|
+
- Reduced gem size
|
9
|
+
|
10
|
+
## 0.3.0 (2020-07-21)
|
11
|
+
|
12
|
+
- Updated OR-Tools to 7.7
|
13
|
+
- Added `BasicScheduler` class
|
14
|
+
- Added `Seating` class
|
15
|
+
- Added `TSP` class
|
16
|
+
- Added `Sudoku` class
|
17
|
+
|
18
|
+
## 0.2.0 (2020-05-22)
|
19
|
+
|
20
|
+
- No longer need to download the OR-Tools C++ library separately on Mac, Ubuntu 18.04, Ubuntu 16.04, Debian 10, and CentOS 8
|
21
|
+
|
22
|
+
## 0.1.5 (2020-04-23)
|
23
|
+
|
24
|
+
- Added support for OR-Tools 7.6
|
25
|
+
|
1
26
|
## 0.1.4 (2020-04-19)
|
2
27
|
|
3
28
|
- Added support for the Job Shop Problem
|
data/README.md
CHANGED
@@ -6,24 +6,261 @@
|
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
9
|
-
|
9
|
+
Add this line to your application’s Gemfile:
|
10
10
|
|
11
|
-
```
|
12
|
-
|
11
|
+
```ruby
|
12
|
+
gem 'or-tools'
|
13
13
|
```
|
14
14
|
|
15
|
-
|
15
|
+
Installation can take a few minutes as OR-Tools downloads and builds.
|
16
|
+
|
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
|
16
27
|
|
17
28
|
```ruby
|
18
|
-
|
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
|
19
196
|
```
|
20
197
|
|
21
|
-
|
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
|
22
255
|
|
23
256
|
Linear Optimization
|
24
257
|
|
25
258
|
- [The Glop Linear Solver](#the-glop-linear-solver)
|
26
259
|
|
260
|
+
Integer Optimization
|
261
|
+
|
262
|
+
- [Mixed-Integer Programming](#mixed-integer-programming)
|
263
|
+
|
27
264
|
Constraint Optimization
|
28
265
|
|
29
266
|
- [CP-SAT Solver](#cp-sat-solver)
|
@@ -31,13 +268,14 @@ Constraint Optimization
|
|
31
268
|
- [Cryptarithmetic](#cryptarithmetic)
|
32
269
|
- [The N-queens Problem](#the-n-queens-problem)
|
33
270
|
|
34
|
-
|
271
|
+
Assignment
|
35
272
|
|
36
|
-
- [
|
273
|
+
- [Assignment](#assignment)
|
274
|
+
- [Assignment with Teams](#assignment-with-teams)
|
37
275
|
|
38
276
|
Routing
|
39
277
|
|
40
|
-
- [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
|
278
|
+
- [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp-1)
|
41
279
|
- [Vehicle Routing Problem (VRP)](#vehicle-routing-problem-vrp)
|
42
280
|
- [Capacity Constraints](#capacity-constraints)
|
43
281
|
- [Pickups and Deliveries](#pickups-and-deliveries)
|
@@ -56,12 +294,7 @@ Network Flows
|
|
56
294
|
|
57
295
|
- [Maximum Flows](#maximum-flows)
|
58
296
|
- [Minimum Cost Flows](#minimum-cost-flows)
|
59
|
-
|
60
|
-
Assignment
|
61
|
-
|
62
|
-
- [Assignment](#assignment)
|
63
|
-
- [Assignment as a Min Cost Problem](#assignment-as-a-min-cost-problem)
|
64
|
-
- [Assignment as a MIP Problem](#assignment-as-a-mip-problem)
|
297
|
+
- [Assignment as a Min Cost Flow Problem](#assignment-as-a-min-cost-flow-problem)
|
65
298
|
|
66
299
|
Scheduling
|
67
300
|
|
@@ -70,7 +303,7 @@ Scheduling
|
|
70
303
|
|
71
304
|
Other Examples
|
72
305
|
|
73
|
-
- [Sudoku](#sudoku)
|
306
|
+
- [Sudoku](#sudoku-1)
|
74
307
|
- [Wedding Seating Chart](#wedding-seating-chart)
|
75
308
|
- [Set Partitioning](#set-partitioning)
|
76
309
|
|
@@ -118,6 +351,52 @@ puts "y = #{y.solution_value}"
|
|
118
351
|
puts "Optimal objective value = #{opt_solution}"
|
119
352
|
```
|
120
353
|
|
354
|
+
### Mixed-Integer Programming
|
355
|
+
|
356
|
+
[Guide](https://developers.google.com/optimization/mip/integer_opt)
|
357
|
+
|
358
|
+
```ruby
|
359
|
+
# declare the MIP solver
|
360
|
+
solver = ORTools::Solver.new("simple_mip_program", :cbc)
|
361
|
+
|
362
|
+
# define the variables
|
363
|
+
infinity = solver.infinity
|
364
|
+
x = solver.int_var(0.0, infinity, "x")
|
365
|
+
y = solver.int_var(0.0, infinity, "y")
|
366
|
+
|
367
|
+
puts "Number of variables = #{solver.num_variables}"
|
368
|
+
|
369
|
+
# define the constraints
|
370
|
+
c0 = solver.constraint(-infinity, 17.5)
|
371
|
+
c0.set_coefficient(x, 1)
|
372
|
+
c0.set_coefficient(y, 7)
|
373
|
+
|
374
|
+
c1 = solver.constraint(-infinity, 3.5)
|
375
|
+
c1.set_coefficient(x, 1);
|
376
|
+
c1.set_coefficient(y, 0);
|
377
|
+
|
378
|
+
puts "Number of constraints = #{solver.num_constraints}"
|
379
|
+
|
380
|
+
# define the objective
|
381
|
+
objective = solver.objective
|
382
|
+
objective.set_coefficient(x, 1)
|
383
|
+
objective.set_coefficient(y, 10)
|
384
|
+
objective.set_maximization
|
385
|
+
|
386
|
+
# call the solver
|
387
|
+
status = solver.solve
|
388
|
+
|
389
|
+
# display the solution
|
390
|
+
if status == :optimal
|
391
|
+
puts "Solution:"
|
392
|
+
puts "Objective value = #{solver.objective.value}"
|
393
|
+
puts "x = #{x.solution_value}"
|
394
|
+
puts "y = #{y.solution_value}"
|
395
|
+
else
|
396
|
+
puts "The problem does not have an optimal solution."
|
397
|
+
end
|
398
|
+
```
|
399
|
+
|
121
400
|
### CP-SAT Solver
|
122
401
|
|
123
402
|
[Guide](https://developers.google.com/optimization/cp/cp_solver)
|
@@ -140,7 +419,7 @@ solver = ORTools::CpSolver.new
|
|
140
419
|
status = solver.solve(model)
|
141
420
|
|
142
421
|
# display the first solution
|
143
|
-
if status == :
|
422
|
+
if status == :optimal
|
144
423
|
puts "x = #{solver.value(x)}"
|
145
424
|
puts "y = #{solver.value(y)}"
|
146
425
|
puts "z = #{solver.value(z)}"
|
@@ -299,50 +578,119 @@ puts
|
|
299
578
|
puts "Solutions found : %i" % solution_printer.solution_count
|
300
579
|
```
|
301
580
|
|
302
|
-
### Mixed-Integer Programming
|
303
581
|
|
304
|
-
|
582
|
+
### Assignment
|
583
|
+
|
584
|
+
[Guide](https://developers.google.com/optimization/assignment/assignment_example)
|
305
585
|
|
306
586
|
```ruby
|
307
|
-
#
|
308
|
-
|
587
|
+
# create the data
|
588
|
+
cost = [[ 90, 76, 75, 70],
|
589
|
+
[ 35, 85, 55, 65],
|
590
|
+
[125, 95, 90, 105],
|
591
|
+
[ 45, 110, 95, 115]]
|
309
592
|
|
310
|
-
|
311
|
-
|
312
|
-
x = solver.int_var(0.0, infinity, "x")
|
313
|
-
y = solver.int_var(0.0, infinity, "y")
|
593
|
+
rows = cost.length
|
594
|
+
cols = cost[0].length
|
314
595
|
|
315
|
-
|
596
|
+
# create the solver
|
597
|
+
assignment = ORTools::LinearSumAssignment.new
|
316
598
|
|
317
|
-
#
|
318
|
-
|
319
|
-
|
320
|
-
|
599
|
+
# add the costs to the solver
|
600
|
+
rows.times do |worker|
|
601
|
+
cols.times do |task|
|
602
|
+
if cost[worker][task]
|
603
|
+
assignment.add_arc_with_cost(worker, task, cost[worker][task])
|
604
|
+
end
|
605
|
+
end
|
606
|
+
end
|
321
607
|
|
322
|
-
|
323
|
-
|
324
|
-
|
608
|
+
# invoke the solver
|
609
|
+
solve_status = assignment.solve
|
610
|
+
if solve_status == :optimal
|
611
|
+
puts "Total cost = #{assignment.optimal_cost}"
|
612
|
+
puts
|
613
|
+
assignment.num_nodes.times do |i|
|
614
|
+
puts "Worker %d assigned to task %d. Cost = %d" % [
|
615
|
+
i,
|
616
|
+
assignment.right_mate(i),
|
617
|
+
assignment.assignment_cost(i)
|
618
|
+
]
|
619
|
+
end
|
620
|
+
elsif solve_status == :infeasible
|
621
|
+
puts "No assignment is possible."
|
622
|
+
elsif solve_status == :possible_overflow
|
623
|
+
puts "Some input costs are too large and may cause an integer overflow."
|
624
|
+
end
|
625
|
+
```
|
325
626
|
|
326
|
-
|
627
|
+
### Assignment with Teams
|
327
628
|
|
328
|
-
|
329
|
-
objective = solver.objective
|
330
|
-
objective.set_coefficient(x, 1)
|
331
|
-
objective.set_coefficient(y, 10)
|
332
|
-
objective.set_maximization
|
629
|
+
[Guide](https://developers.google.com/optimization/assignment/assignment_teams)
|
333
630
|
|
334
|
-
|
335
|
-
|
631
|
+
```ruby
|
632
|
+
# create the solver
|
633
|
+
solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
|
336
634
|
|
337
|
-
#
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
635
|
+
# create the data
|
636
|
+
cost = [[90, 76, 75, 70],
|
637
|
+
[35, 85, 55, 65],
|
638
|
+
[125, 95, 90, 105],
|
639
|
+
[45, 110, 95, 115],
|
640
|
+
[60, 105, 80, 75],
|
641
|
+
[45, 65, 110, 95]]
|
642
|
+
|
643
|
+
team1 = [0, 2, 4]
|
644
|
+
team2 = [1, 3, 5]
|
645
|
+
team_max = 2
|
646
|
+
|
647
|
+
# create the variables
|
648
|
+
num_workers = cost.length
|
649
|
+
num_tasks = cost[1].length
|
650
|
+
x = {}
|
651
|
+
|
652
|
+
num_workers.times do |i|
|
653
|
+
num_tasks.times do |j|
|
654
|
+
x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
# create the objective function
|
659
|
+
solver.minimize(solver.sum(
|
660
|
+
num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
|
661
|
+
))
|
662
|
+
|
663
|
+
# create the constraints
|
664
|
+
num_workers.times do |i|
|
665
|
+
solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
|
666
|
+
end
|
667
|
+
|
668
|
+
num_tasks.times do |j|
|
669
|
+
solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
|
670
|
+
end
|
671
|
+
|
672
|
+
solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
|
673
|
+
solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
|
674
|
+
|
675
|
+
# invoke the solver
|
676
|
+
sol = solver.solve
|
677
|
+
|
678
|
+
puts "Total cost = #{solver.objective.value}"
|
679
|
+
puts
|
680
|
+
num_workers.times do |i|
|
681
|
+
num_tasks.times do |j|
|
682
|
+
if x[[i, j]].solution_value > 0
|
683
|
+
puts "Worker %d assigned to task %d. Cost = %d" % [
|
684
|
+
i,
|
685
|
+
j,
|
686
|
+
cost[i][j]
|
687
|
+
]
|
688
|
+
end
|
689
|
+
end
|
345
690
|
end
|
691
|
+
|
692
|
+
puts
|
693
|
+
puts "Time = #{solver.wall_time} milliseconds"
|
346
694
|
```
|
347
695
|
|
348
696
|
### Traveling Salesperson Problem (TSP)
|
@@ -384,7 +732,7 @@ transit_callback_index = routing.register_transit_callback(distance_callback)
|
|
384
732
|
routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
|
385
733
|
|
386
734
|
# run the solver
|
387
|
-
assignment = routing.solve(first_solution_strategy: :
|
735
|
+
assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
|
388
736
|
|
389
737
|
# print the solution
|
390
738
|
puts "Objective: #{assignment.objective_value} miles"
|
@@ -1280,52 +1628,7 @@ else
|
|
1280
1628
|
end
|
1281
1629
|
```
|
1282
1630
|
|
1283
|
-
### Assignment
|
1284
|
-
|
1285
|
-
[Guide](https://developers.google.com/optimization/assignment/simple_assignment)
|
1286
|
-
|
1287
|
-
```ruby
|
1288
|
-
# create the data
|
1289
|
-
cost = [[ 90, 76, 75, 70],
|
1290
|
-
[ 35, 85, 55, 65],
|
1291
|
-
[125, 95, 90, 105],
|
1292
|
-
[ 45, 110, 95, 115]]
|
1293
|
-
|
1294
|
-
rows = cost.length
|
1295
|
-
cols = cost[0].length
|
1296
|
-
|
1297
|
-
# create the solver
|
1298
|
-
assignment = ORTools::LinearSumAssignment.new
|
1299
|
-
|
1300
|
-
# add the costs to the solver
|
1301
|
-
rows.times do |worker|
|
1302
|
-
cols.times do |task|
|
1303
|
-
if cost[worker][task]
|
1304
|
-
assignment.add_arc_with_cost(worker, task, cost[worker][task])
|
1305
|
-
end
|
1306
|
-
end
|
1307
|
-
end
|
1308
|
-
|
1309
|
-
# invoke the solver
|
1310
|
-
solve_status = assignment.solve
|
1311
|
-
if solve_status == :optimal
|
1312
|
-
puts "Total cost = #{assignment.optimal_cost}"
|
1313
|
-
puts
|
1314
|
-
assignment.num_nodes.times do |i|
|
1315
|
-
puts "Worker %d assigned to task %d. Cost = %d" % [
|
1316
|
-
i,
|
1317
|
-
assignment.right_mate(i),
|
1318
|
-
assignment.assignment_cost(i)
|
1319
|
-
]
|
1320
|
-
end
|
1321
|
-
elsif solve_status == :infeasible
|
1322
|
-
puts "No assignment is possible."
|
1323
|
-
elsif solve_status == :possible_overflow
|
1324
|
-
puts "Some input costs are too large and may cause an integer overflow."
|
1325
|
-
end
|
1326
|
-
```
|
1327
|
-
|
1328
|
-
### Assignment as a Min Cost Problem
|
1631
|
+
### Assignment as a Min Cost Flow Problem
|
1329
1632
|
|
1330
1633
|
[Guide](https://developers.google.com/optimization/assignment/assignment_min_cost_flow)
|
1331
1634
|
|
@@ -1374,75 +1677,6 @@ else
|
|
1374
1677
|
end
|
1375
1678
|
```
|
1376
1679
|
|
1377
|
-
### Assignment as a MIP Problem
|
1378
|
-
|
1379
|
-
[Guide](https://developers.google.com/optimization/assignment/assignment_mip)
|
1380
|
-
|
1381
|
-
```ruby
|
1382
|
-
# create the solver
|
1383
|
-
solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
|
1384
|
-
|
1385
|
-
# create the data
|
1386
|
-
cost = [[90, 76, 75, 70],
|
1387
|
-
[35, 85, 55, 65],
|
1388
|
-
[125, 95, 90, 105],
|
1389
|
-
[45, 110, 95, 115],
|
1390
|
-
[60, 105, 80, 75],
|
1391
|
-
[45, 65, 110, 95]]
|
1392
|
-
|
1393
|
-
team1 = [0, 2, 4]
|
1394
|
-
team2 = [1, 3, 5]
|
1395
|
-
team_max = 2
|
1396
|
-
|
1397
|
-
# create the variables
|
1398
|
-
num_workers = cost.length
|
1399
|
-
num_tasks = cost[1].length
|
1400
|
-
x = {}
|
1401
|
-
|
1402
|
-
num_workers.times do |i|
|
1403
|
-
num_tasks.times do |j|
|
1404
|
-
x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
|
1405
|
-
end
|
1406
|
-
end
|
1407
|
-
|
1408
|
-
# create the objective function
|
1409
|
-
solver.minimize(solver.sum(
|
1410
|
-
num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
|
1411
|
-
))
|
1412
|
-
|
1413
|
-
# create the constraints
|
1414
|
-
num_workers.times do |i|
|
1415
|
-
solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
|
1416
|
-
end
|
1417
|
-
|
1418
|
-
num_tasks.times do |j|
|
1419
|
-
solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
|
1420
|
-
end
|
1421
|
-
|
1422
|
-
solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
|
1423
|
-
solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
|
1424
|
-
|
1425
|
-
# invoke the solver
|
1426
|
-
sol = solver.solve
|
1427
|
-
|
1428
|
-
puts "Total cost = #{solver.objective.value}"
|
1429
|
-
puts
|
1430
|
-
num_workers.times do |i|
|
1431
|
-
num_tasks.times do |j|
|
1432
|
-
if x[[i, j]].solution_value > 0
|
1433
|
-
puts "Worker %d assigned to task %d. Cost = %d" % [
|
1434
|
-
i,
|
1435
|
-
j,
|
1436
|
-
cost[i][j]
|
1437
|
-
]
|
1438
|
-
end
|
1439
|
-
end
|
1440
|
-
end
|
1441
|
-
|
1442
|
-
puts
|
1443
|
-
puts "Time = #{solver.wall_time} milliseconds"
|
1444
|
-
```
|
1445
|
-
|
1446
1680
|
### Employee Scheduling
|
1447
1681
|
|
1448
1682
|
[Guide](https://developers.google.com/optimization/scheduling/employee_scheduling)
|
@@ -1714,7 +1948,7 @@ end
|
|
1714
1948
|
# solve and print solution
|
1715
1949
|
solver = ORTools::CpSolver.new
|
1716
1950
|
status = solver.solve(model)
|
1717
|
-
if status == :
|
1951
|
+
if status == :optimal
|
1718
1952
|
line.each do |i|
|
1719
1953
|
p line.map { |j| solver.value(grid[[i, j]]) }
|
1720
1954
|
end
|
@@ -1729,8 +1963,8 @@ end
|
|
1729
1963
|
# From
|
1730
1964
|
# Meghan L. Bellows and J. D. Luc Peterson
|
1731
1965
|
# "Finding an optimal seating chart for a wedding"
|
1732
|
-
#
|
1733
|
-
#
|
1966
|
+
# https://www.improbable.com/news/2012/Optimal-seating-chart.pdf
|
1967
|
+
# https://www.improbable.com/2012/02/12/finding-an-optimal-seating-chart-for-a-wedding
|
1734
1968
|
#
|
1735
1969
|
# Every year, millions of brides (not to mention their mothers, future
|
1736
1970
|
# mothers-in-law, and occasionally grooms) struggle with one of the
|
@@ -1803,19 +2037,17 @@ all_tables.each do |t|
|
|
1803
2037
|
end
|
1804
2038
|
end
|
1805
2039
|
|
2040
|
+
pairs = all_guests.combination(2)
|
2041
|
+
|
1806
2042
|
colocated = {}
|
1807
|
-
|
1808
|
-
|
1809
|
-
colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
|
1810
|
-
end
|
2043
|
+
pairs.each do |g1, g2|
|
2044
|
+
colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
|
1811
2045
|
end
|
1812
2046
|
|
1813
2047
|
same_table = {}
|
1814
|
-
|
1815
|
-
|
1816
|
-
|
1817
|
-
same_table[[g1, g2, t]] = model.new_bool_var("guest %i seats with guest %i on table %i" % [g1, g2, t])
|
1818
|
-
end
|
2048
|
+
pairs.each do |g1, g2|
|
2049
|
+
all_tables.each do |t|
|
2050
|
+
same_table[[g1, g2, t]] = model.new_bool_var("guest %i seats with guest %i on table %i" % [g1, g2, t])
|
1819
2051
|
end
|
1820
2052
|
end
|
1821
2053
|
|
@@ -1837,29 +2069,32 @@ all_tables.each do |t|
|
|
1837
2069
|
end
|
1838
2070
|
|
1839
2071
|
# Link colocated with seats
|
1840
|
-
|
1841
|
-
|
1842
|
-
|
1843
|
-
|
1844
|
-
|
1845
|
-
|
1846
|
-
model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])
|
1847
|
-
end
|
1848
|
-
|
1849
|
-
# Link colocated and same_table.
|
1850
|
-
model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])
|
2072
|
+
pairs.each do |g1, g2|
|
2073
|
+
all_tables.each do |t|
|
2074
|
+
# Link same_table and seats.
|
2075
|
+
model.add_bool_or([seats[[t, g1]].not, seats[[t, g2]].not, same_table[[g1, g2, t]]])
|
2076
|
+
model.add_implication(same_table[[g1, g2, t]], seats[[t, g1]])
|
2077
|
+
model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])
|
1851
2078
|
end
|
2079
|
+
|
2080
|
+
# Link colocated and same_table.
|
2081
|
+
model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])
|
1852
2082
|
end
|
1853
2083
|
|
1854
2084
|
# Min known neighbors rule.
|
1855
|
-
|
2085
|
+
all_guests.each do |g|
|
1856
2086
|
model.add(
|
1857
2087
|
model.sum(
|
1858
|
-
(num_guests - 1).
|
1859
|
-
|
1860
|
-
|
1861
|
-
|
1862
|
-
|
2088
|
+
(g + 1).upto(num_guests - 1).
|
2089
|
+
select { |g2| c[g][g2] > 0 }.
|
2090
|
+
product(all_tables).
|
2091
|
+
map { |g2, t| same_table[[g, g2, t]] }
|
2092
|
+
) +
|
2093
|
+
model.sum(
|
2094
|
+
g.times.
|
2095
|
+
select { |g1| c[g1][g] > 0 }.
|
2096
|
+
product(all_tables).
|
2097
|
+
map { |g1, t| same_table[[g1, g, t]] }
|
1863
2098
|
) >= min_known_neighbors
|
1864
2099
|
)
|
1865
2100
|
end
|
@@ -1951,7 +2186,7 @@ To get started with development:
|
|
1951
2186
|
git clone https://github.com/ankane/or-tools.git
|
1952
2187
|
cd or-tools
|
1953
2188
|
bundle install
|
1954
|
-
bundle exec rake compile
|
2189
|
+
bundle exec rake compile
|
1955
2190
|
bundle exec rake test
|
1956
2191
|
```
|
1957
2192
|
|