or-tools 0.1.4 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|