or-tools 0.1.2 → 0.3.0
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 +1505 -423
- data/ext/or-tools/ext.cpp +280 -18
- data/ext/or-tools/extconf.rb +21 -63
- data/ext/or-tools/vendor.rb +95 -0
- data/lib/or-tools.rb +8 -0
- data/lib/or_tools/basic_scheduler.rb +86 -0
- data/lib/or_tools/bool_var.rb +9 -0
- data/lib/or_tools/cp_solver.rb +11 -2
- data/lib/or_tools/cp_solver_solution_callback.rb +12 -1
- data/lib/or_tools/ext.bundle +0 -0
- data/lib/or_tools/int_var.rb +5 -0
- data/lib/or_tools/linear_expr.rb +8 -0
- data/lib/or_tools/sat_int_var.rb +13 -0
- data/lib/or_tools/sat_linear_expr.rb +18 -2
- 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 +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 18b9ab384a5aa4b8d27a3415de9ba751085e63b95cbdc431d1fee206b05e1c37
|
4
|
+
data.tar.gz: 4b8d29c3f6b5fff8181a817737a818e2c3d7cf6cd0184d82078d4606c2c94740
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26e5b155bdd5c0d37aaf13fe26c674ac15ac693933999056a477e340823ced7e86b8565c00c941d4ecdfd35e0b5a4716465e2418bf1e44ca7c1586d171e77515
|
7
|
+
data.tar.gz: 1c87b6df5082d9ab61b1ab63d154e563fdd9405fa3dda5fd00df2f6aafc65c7f5f8d251938cab9cba58de04efb7f38b11893701c128833059b8290b3a3dfb0da
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,28 @@
|
|
1
|
+
## 0.3.0 (2020-07-21)
|
2
|
+
|
3
|
+
- Updated OR-Tools to 7.7
|
4
|
+
- Added `BasicScheduler` class
|
5
|
+
- Added `Seating` class
|
6
|
+
- Added `TSP` class
|
7
|
+
- Added `Sudoku` class
|
8
|
+
|
9
|
+
## 0.2.0 (2020-05-22)
|
10
|
+
|
11
|
+
- No longer need to download the OR-Tools C++ library separately on Mac, Ubuntu 18.04, Ubuntu 16.04, Debian 10, and CentOS 8
|
12
|
+
|
13
|
+
## 0.1.5 (2020-04-23)
|
14
|
+
|
15
|
+
- Added support for OR-Tools 7.6
|
16
|
+
|
17
|
+
## 0.1.4 (2020-04-19)
|
18
|
+
|
19
|
+
- Added support for the Job Shop Problem
|
20
|
+
|
21
|
+
## 0.1.3 (2020-03-24)
|
22
|
+
|
23
|
+
- Added support for more routing problems
|
24
|
+
- Added `add_all_different` to `CpModel`
|
25
|
+
|
1
26
|
## 0.1.2 (2020-02-18)
|
2
27
|
|
3
28
|
- Added support for scheduling
|
data/README.md
CHANGED
@@ -6,37 +6,282 @@
|
|
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
|
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
|
16
48
|
|
17
49
|
```ruby
|
18
|
-
|
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
|
19
252
|
```
|
20
253
|
|
21
|
-
##
|
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)
|
30
267
|
- [Solving an Optimization Problem](#solving-an-optimization-problem)
|
268
|
+
- [Cryptarithmetic](#cryptarithmetic)
|
269
|
+
- [The N-queens Problem](#the-n-queens-problem)
|
31
270
|
|
32
|
-
|
271
|
+
Assignment
|
33
272
|
|
34
|
-
- [
|
273
|
+
- [Assignment](#assignment)
|
274
|
+
- [Assignment with Teams](#assignment-with-teams)
|
35
275
|
|
36
276
|
Routing
|
37
277
|
|
38
|
-
- [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
|
278
|
+
- [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp-1)
|
39
279
|
- [Vehicle Routing Problem (VRP)](#vehicle-routing-problem-vrp)
|
280
|
+
- [Capacity Constraints](#capacity-constraints)
|
281
|
+
- [Pickups and Deliveries](#pickups-and-deliveries)
|
282
|
+
- [Time Window Constraints](#time-window-constraints)
|
283
|
+
- [Resource Constraints](#resource-constraints)
|
284
|
+
- [Penalties and Dropping Visits](#penalties-and-dropping-visits)
|
40
285
|
- [Routing Options](#routing-options)
|
41
286
|
|
42
287
|
Bin Packing
|
@@ -49,37 +294,32 @@ Network Flows
|
|
49
294
|
|
50
295
|
- [Maximum Flows](#maximum-flows)
|
51
296
|
- [Minimum Cost Flows](#minimum-cost-flows)
|
52
|
-
|
53
|
-
Assignment
|
54
|
-
|
55
|
-
- [Assignment](#assignment)
|
56
|
-
- [Assignment as a Min Cost Problem](#assignment-as-a-min-cost-problem)
|
57
|
-
- [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)
|
58
298
|
|
59
299
|
Scheduling
|
60
300
|
|
61
301
|
- [Employee Scheduling](#employee-scheduling)
|
302
|
+
- [The Job Shop Problem](#the-job-shop-problem)
|
303
|
+
|
304
|
+
Other Examples
|
305
|
+
|
306
|
+
- [Sudoku](#sudoku-1)
|
307
|
+
- [Wedding Seating Chart](#wedding-seating-chart)
|
308
|
+
- [Set Partitioning](#set-partitioning)
|
62
309
|
|
63
310
|
### The Glop Linear Solver
|
64
311
|
|
65
312
|
[Guide](https://developers.google.com/optimization/lp/glop)
|
66
313
|
|
67
|
-
Declare the solver
|
68
|
-
|
69
314
|
```ruby
|
315
|
+
# declare the solver
|
70
316
|
solver = ORTools::Solver.new("LinearProgrammingExample", :glop)
|
71
|
-
```
|
72
|
-
|
73
|
-
Create the variables
|
74
317
|
|
75
|
-
|
318
|
+
# create the variables
|
76
319
|
x = solver.num_var(0, solver.infinity, "x")
|
77
320
|
y = solver.num_var(0, solver.infinity, "y")
|
78
|
-
```
|
79
321
|
|
80
|
-
|
81
|
-
|
82
|
-
```ruby
|
322
|
+
# define the constraints
|
83
323
|
constraint0 = solver.constraint(-solver.infinity, 14)
|
84
324
|
constraint0.set_coefficient(x, 1)
|
85
325
|
constraint0.set_coefficient(y, 2)
|
@@ -91,26 +331,17 @@ constraint1.set_coefficient(y, -1)
|
|
91
331
|
constraint2 = solver.constraint(-solver.infinity, 2)
|
92
332
|
constraint2.set_coefficient(x, 1)
|
93
333
|
constraint2.set_coefficient(y, -1)
|
94
|
-
```
|
95
|
-
|
96
|
-
Define the objective function
|
97
334
|
|
98
|
-
|
335
|
+
# define the objective function
|
99
336
|
objective = solver.objective
|
100
337
|
objective.set_coefficient(x, 3)
|
101
338
|
objective.set_coefficient(y, 4)
|
102
339
|
objective.set_maximization
|
103
|
-
```
|
104
340
|
|
105
|
-
|
106
|
-
|
107
|
-
```ruby
|
341
|
+
# invoke the solver
|
108
342
|
solver.solve
|
109
|
-
```
|
110
|
-
|
111
|
-
Display the solution
|
112
343
|
|
113
|
-
|
344
|
+
# display the solution
|
114
345
|
opt_solution = 3 * x.solution_value + 4 * y.solution_value
|
115
346
|
puts "Number of variables = #{solver.num_variables}"
|
116
347
|
puts "Number of constraints = #{solver.num_constraints}"
|
@@ -120,42 +351,75 @@ puts "y = #{y.solution_value}"
|
|
120
351
|
puts "Optimal objective value = #{opt_solution}"
|
121
352
|
```
|
122
353
|
|
123
|
-
###
|
124
|
-
|
125
|
-
[Guide](https://developers.google.com/optimization/cp/cp_solver)
|
354
|
+
### Mixed-Integer Programming
|
126
355
|
|
127
|
-
|
356
|
+
[Guide](https://developers.google.com/optimization/mip/integer_opt)
|
128
357
|
|
129
358
|
```ruby
|
130
|
-
|
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
|
131
398
|
```
|
132
399
|
|
133
|
-
|
400
|
+
### CP-SAT Solver
|
401
|
+
|
402
|
+
[Guide](https://developers.google.com/optimization/cp/cp_solver)
|
134
403
|
|
135
404
|
```ruby
|
405
|
+
# declare the model
|
406
|
+
model = ORTools::CpModel.new
|
407
|
+
|
408
|
+
# create the variables
|
136
409
|
num_vals = 3
|
137
410
|
x = model.new_int_var(0, num_vals - 1, "x")
|
138
411
|
y = model.new_int_var(0, num_vals - 1, "y")
|
139
412
|
z = model.new_int_var(0, num_vals - 1, "z")
|
140
|
-
```
|
141
413
|
|
142
|
-
|
143
|
-
|
144
|
-
```ruby
|
414
|
+
# create the constraint
|
145
415
|
model.add(x != y)
|
146
|
-
```
|
147
|
-
|
148
|
-
Call the solver
|
149
416
|
|
150
|
-
|
417
|
+
# call the solver
|
151
418
|
solver = ORTools::CpSolver.new
|
152
419
|
status = solver.solve(model)
|
153
|
-
```
|
154
420
|
|
155
|
-
|
156
|
-
|
157
|
-
```ruby
|
158
|
-
if status == :feasible
|
421
|
+
# display the first solution
|
422
|
+
if status == :optimal
|
159
423
|
puts "x = #{solver.value(x)}"
|
160
424
|
puts "y = #{solver.value(y)}"
|
161
425
|
puts "z = #{solver.value(z)}"
|
@@ -166,38 +430,25 @@ end
|
|
166
430
|
|
167
431
|
[Guide](https://developers.google.com/optimization/cp/integer_opt_cp)
|
168
432
|
|
169
|
-
Declare the model
|
170
|
-
|
171
433
|
```ruby
|
434
|
+
# declare the model
|
172
435
|
model = ORTools::CpModel.new
|
173
|
-
```
|
174
|
-
|
175
|
-
Create the variables
|
176
436
|
|
177
|
-
|
437
|
+
# create the variables
|
178
438
|
var_upper_bound = [50, 45, 37].max
|
179
439
|
x = model.new_int_var(0, var_upper_bound, "x")
|
180
440
|
y = model.new_int_var(0, var_upper_bound, "y")
|
181
441
|
z = model.new_int_var(0, var_upper_bound, "z")
|
182
|
-
```
|
183
442
|
|
184
|
-
|
185
|
-
|
186
|
-
```ruby
|
443
|
+
# define the constraints
|
187
444
|
model.add(x*2 + y*7 + z*3 <= 50)
|
188
445
|
model.add(x*3 - y*5 + z*7 <= 45)
|
189
446
|
model.add(x*5 + y*2 - z*6 <= 37)
|
190
|
-
```
|
191
|
-
|
192
|
-
Define the objective function
|
193
447
|
|
194
|
-
|
448
|
+
# define the objective function
|
195
449
|
model.maximize(x*2 + y*2 + z*3)
|
196
|
-
```
|
197
450
|
|
198
|
-
|
199
|
-
|
200
|
-
```ruby
|
451
|
+
# call the solver
|
201
452
|
solver = ORTools::CpSolver.new
|
202
453
|
status = solver.solve(model)
|
203
454
|
|
@@ -210,75 +461,244 @@ if status == :optimal
|
|
210
461
|
end
|
211
462
|
```
|
212
463
|
|
213
|
-
###
|
214
|
-
|
215
|
-
[Guide](https://developers.google.com/optimization/mip/integer_opt)
|
464
|
+
### Cryptarithmetic
|
216
465
|
|
217
|
-
|
466
|
+
[Guide](https://developers.google.com/optimization/cp/cryptarithmetic)
|
218
467
|
|
219
468
|
```ruby
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
Define the variables
|
469
|
+
# define the variables
|
470
|
+
model = ORTools::CpModel.new
|
224
471
|
|
225
|
-
|
226
|
-
infinity = solver.infinity
|
227
|
-
x = solver.int_var(0.0, infinity, "x")
|
228
|
-
y = solver.int_var(0.0, infinity, "y")
|
472
|
+
base = 10
|
229
473
|
|
230
|
-
|
231
|
-
|
474
|
+
c = model.new_int_var(1, base - 1, "C")
|
475
|
+
p = model.new_int_var(0, base - 1, "P")
|
476
|
+
i = model.new_int_var(1, base - 1, "I")
|
477
|
+
s = model.new_int_var(0, base - 1, "S")
|
478
|
+
f = model.new_int_var(1, base - 1, "F")
|
479
|
+
u = model.new_int_var(0, base - 1, "U")
|
480
|
+
n = model.new_int_var(0, base - 1, "N")
|
481
|
+
t = model.new_int_var(1, base - 1, "T")
|
482
|
+
r = model.new_int_var(0, base - 1, "R")
|
483
|
+
e = model.new_int_var(0, base - 1, "E")
|
232
484
|
|
233
|
-
|
485
|
+
letters = [c, p, i, s, f, u, n, t, r, e]
|
234
486
|
|
235
|
-
|
236
|
-
|
237
|
-
c0.set_coefficient(x, 1)
|
238
|
-
c0.set_coefficient(y, 7)
|
487
|
+
# define the constraints
|
488
|
+
model.add_all_different(letters)
|
239
489
|
|
240
|
-
|
241
|
-
|
242
|
-
c1.set_coefficient(y, 0);
|
490
|
+
model.add(c * base + p + i * base + s + f * base * base + u * base +
|
491
|
+
n == t * base * base * base + r * base * base + u * base + e)
|
243
492
|
|
244
|
-
|
245
|
-
|
493
|
+
# define the solution printer
|
494
|
+
class VarArraySolutionPrinter < ORTools::CpSolverSolutionCallback
|
495
|
+
attr_reader :solution_count
|
246
496
|
|
247
|
-
|
497
|
+
def initialize(variables)
|
498
|
+
super()
|
499
|
+
@variables = variables
|
500
|
+
@solution_count = 0
|
501
|
+
end
|
248
502
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
503
|
+
def on_solution_callback
|
504
|
+
@solution_count += 1
|
505
|
+
@variables.each do |v|
|
506
|
+
print "%s=%i " % [v.name, value(v)]
|
507
|
+
end
|
508
|
+
puts
|
509
|
+
end
|
510
|
+
end
|
255
511
|
|
256
|
-
|
512
|
+
# invoke the solver
|
513
|
+
solver = ORTools::CpSolver.new
|
514
|
+
solution_printer = VarArraySolutionPrinter.new(letters)
|
515
|
+
status = solver.search_for_all_solutions(model, solution_printer)
|
257
516
|
|
258
|
-
|
259
|
-
|
517
|
+
puts
|
518
|
+
puts "Statistics"
|
519
|
+
puts " - status : %s" % status
|
520
|
+
puts " - conflicts : %i" % solver.num_conflicts
|
521
|
+
puts " - branches : %i" % solver.num_branches
|
522
|
+
puts " - wall time : %f s" % solver.wall_time
|
523
|
+
puts " - solutions found : %i" % solution_printer.solution_count
|
260
524
|
```
|
261
525
|
|
262
|
-
|
526
|
+
### The N-queens Problem
|
527
|
+
|
528
|
+
[Guide](https://developers.google.com/optimization/cp/queens)
|
263
529
|
|
264
530
|
```ruby
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
531
|
+
# declare the model
|
532
|
+
board_size = 8
|
533
|
+
model = ORTools::CpModel.new
|
534
|
+
|
535
|
+
# create the variables
|
536
|
+
queens = board_size.times.map { |i| model.new_int_var(0, board_size - 1, "x%i" % i) }
|
537
|
+
|
538
|
+
# create the constraints
|
539
|
+
board_size.times do |i|
|
540
|
+
diag1 = []
|
541
|
+
diag2 = []
|
542
|
+
board_size.times do |j|
|
543
|
+
q1 = model.new_int_var(0, 2 * board_size, "diag1_%i" % i)
|
544
|
+
diag1 << q1
|
545
|
+
model.add(q1 == queens[j] + j)
|
546
|
+
q2 = model.new_int_var(-board_size, board_size, "diag2_%i" % i)
|
547
|
+
diag2 << q2
|
548
|
+
model.add(q2 == queens[j] - j)
|
549
|
+
end
|
550
|
+
model.add_all_different(diag1)
|
551
|
+
model.add_all_different(diag2)
|
552
|
+
end
|
553
|
+
|
554
|
+
# create a solution printer
|
555
|
+
class SolutionPrinter < ORTools::CpSolverSolutionCallback
|
556
|
+
attr_reader :solution_count
|
557
|
+
|
558
|
+
def initialize(variables)
|
559
|
+
super()
|
560
|
+
@variables = variables
|
561
|
+
@solution_count = 0
|
562
|
+
end
|
563
|
+
|
564
|
+
def on_solution_callback
|
565
|
+
@solution_count += 1
|
566
|
+
@variables.each do |v|
|
567
|
+
print "%s = %i " % [v.name, value(v)]
|
568
|
+
end
|
569
|
+
puts
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
# call the solver and display the results
|
574
|
+
solver = ORTools::CpSolver.new
|
575
|
+
solution_printer = SolutionPrinter.new(queens)
|
576
|
+
status = solver.search_for_all_solutions(model, solution_printer)
|
577
|
+
puts
|
578
|
+
puts "Solutions found : %i" % solution_printer.solution_count
|
579
|
+
```
|
580
|
+
|
581
|
+
|
582
|
+
### Assignment
|
583
|
+
|
584
|
+
[Guide](https://developers.google.com/optimization/assignment/assignment_example)
|
585
|
+
|
586
|
+
```ruby
|
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]]
|
592
|
+
|
593
|
+
rows = cost.length
|
594
|
+
cols = cost[0].length
|
595
|
+
|
596
|
+
# create the solver
|
597
|
+
assignment = ORTools::LinearSumAssignment.new
|
598
|
+
|
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
|
607
|
+
|
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
|
+
```
|
626
|
+
|
627
|
+
### Assignment with Teams
|
628
|
+
|
629
|
+
[Guide](https://developers.google.com/optimization/assignment/assignment_teams)
|
630
|
+
|
631
|
+
```ruby
|
632
|
+
# create the solver
|
633
|
+
solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
|
634
|
+
|
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
|
690
|
+
end
|
691
|
+
|
692
|
+
puts
|
693
|
+
puts "Time = #{solver.wall_time} milliseconds"
|
694
|
+
```
|
274
695
|
|
275
696
|
### Traveling Salesperson Problem (TSP)
|
276
697
|
|
277
698
|
[Guide](https://developers.google.com/optimization/routing/tsp.html)
|
278
699
|
|
279
|
-
Create the data
|
280
|
-
|
281
700
|
```ruby
|
701
|
+
# create the data
|
282
702
|
data = {}
|
283
703
|
data[:distance_matrix] = [
|
284
704
|
[0, 2451, 713, 1018, 1631, 1374, 2408, 213, 2571, 875, 1420, 2145, 1972],
|
@@ -297,11 +717,8 @@ data[:distance_matrix] = [
|
|
297
717
|
]
|
298
718
|
data[:num_vehicles] = 1
|
299
719
|
data[:depot] = 0
|
300
|
-
```
|
301
|
-
|
302
|
-
Create the distance callback
|
303
720
|
|
304
|
-
|
721
|
+
# create the distance callback
|
305
722
|
manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].length, data[:num_vehicles], data[:depot])
|
306
723
|
routing = ORTools::RoutingModel.new(manager)
|
307
724
|
|
@@ -313,17 +730,11 @@ end
|
|
313
730
|
|
314
731
|
transit_callback_index = routing.register_transit_callback(distance_callback)
|
315
732
|
routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
|
316
|
-
```
|
317
|
-
|
318
|
-
Run the solver
|
319
|
-
|
320
|
-
```ruby
|
321
|
-
assignment = routing.solve(first_solution_strategy: :path_cheaper_arc)
|
322
|
-
```
|
323
733
|
|
324
|
-
|
734
|
+
# run the solver
|
735
|
+
assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
|
325
736
|
|
326
|
-
|
737
|
+
# print the solution
|
327
738
|
puts "Objective: #{assignment.objective_value} miles"
|
328
739
|
index = routing.start(0)
|
329
740
|
plan_output = String.new("Route for vehicle 0:\n")
|
@@ -342,9 +753,8 @@ puts plan_output
|
|
342
753
|
|
343
754
|
[Guide](https://developers.google.com/optimization/routing/vrp)
|
344
755
|
|
345
|
-
Create the data
|
346
|
-
|
347
756
|
```ruby
|
757
|
+
# create the data
|
348
758
|
data = {}
|
349
759
|
data[:distance_matrix] = [
|
350
760
|
[0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],
|
@@ -367,11 +777,8 @@ data[:distance_matrix] = [
|
|
367
777
|
]
|
368
778
|
data[:num_vehicles] = 4
|
369
779
|
data[:depot] = 0
|
370
|
-
```
|
371
|
-
|
372
|
-
Define the distance callback
|
373
780
|
|
374
|
-
|
781
|
+
# define the distance callback
|
375
782
|
manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].length, data[:num_vehicles], data[:depot])
|
376
783
|
routing = ORTools::RoutingModel.new(manager)
|
377
784
|
|
@@ -383,27 +790,193 @@ end
|
|
383
790
|
|
384
791
|
transit_callback_index = routing.register_transit_callback(distance_callback)
|
385
792
|
routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
|
386
|
-
```
|
387
793
|
|
388
|
-
|
389
|
-
|
390
|
-
```ruby
|
794
|
+
# add a distance dimension
|
391
795
|
dimension_name = "Distance"
|
392
796
|
routing.add_dimension(transit_callback_index, 0, 3000, true, dimension_name)
|
393
797
|
distance_dimension = routing.mutable_dimension(dimension_name)
|
394
798
|
distance_dimension.global_span_cost_coefficient = 100
|
799
|
+
|
800
|
+
# run the solver
|
801
|
+
solution = routing.solve(first_solution_strategy: :path_cheapest_arc)
|
802
|
+
|
803
|
+
# print the solution
|
804
|
+
max_route_distance = 0
|
805
|
+
data[:num_vehicles].times do |vehicle_id|
|
806
|
+
index = routing.start(vehicle_id)
|
807
|
+
plan_output = String.new("Route for vehicle #{vehicle_id}:\n")
|
808
|
+
route_distance = 0
|
809
|
+
while !routing.end?(index)
|
810
|
+
plan_output += " #{manager.index_to_node(index)} -> "
|
811
|
+
previous_index = index
|
812
|
+
index = solution.value(routing.next_var(index))
|
813
|
+
route_distance += routing.arc_cost_for_vehicle(previous_index, index, vehicle_id)
|
814
|
+
end
|
815
|
+
plan_output += "#{manager.index_to_node(index)}\n"
|
816
|
+
plan_output += "Distance of the route: #{route_distance}m\n\n"
|
817
|
+
puts plan_output
|
818
|
+
max_route_distance = [route_distance, max_route_distance].max
|
819
|
+
end
|
820
|
+
puts "Maximum of the route distances: #{max_route_distance}m"
|
395
821
|
```
|
396
822
|
|
397
|
-
|
823
|
+
### Capacity Constraints
|
824
|
+
|
825
|
+
[Guide](https://developers.google.com/optimization/routing/cvrp)
|
398
826
|
|
399
827
|
```ruby
|
828
|
+
data = {}
|
829
|
+
data[:distance_matrix] = [
|
830
|
+
[0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],
|
831
|
+
[548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210],
|
832
|
+
[776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754],
|
833
|
+
[696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358],
|
834
|
+
[582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244],
|
835
|
+
[274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708],
|
836
|
+
[502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480],
|
837
|
+
[194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856],
|
838
|
+
[308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514],
|
839
|
+
[194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468],
|
840
|
+
[536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354],
|
841
|
+
[502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844],
|
842
|
+
[388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730],
|
843
|
+
[354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536],
|
844
|
+
[468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194],
|
845
|
+
[776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798],
|
846
|
+
[662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0]
|
847
|
+
]
|
848
|
+
data[:demands] = [0, 1, 1, 2, 4, 2, 4, 8, 8, 1, 2, 1, 2, 4, 4, 8, 8]
|
849
|
+
data[:vehicle_capacities] = [15, 15, 15, 15]
|
850
|
+
data[:num_vehicles] = 4
|
851
|
+
data[:depot] = 0
|
852
|
+
|
853
|
+
manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].size, data[:num_vehicles], data[:depot])
|
854
|
+
routing = ORTools::RoutingModel.new(manager)
|
855
|
+
|
856
|
+
distance_callback = lambda do |from_index, to_index|
|
857
|
+
from_node = manager.index_to_node(from_index)
|
858
|
+
to_node = manager.index_to_node(to_index)
|
859
|
+
data[:distance_matrix][from_node][to_node]
|
860
|
+
end
|
861
|
+
|
862
|
+
transit_callback_index = routing.register_transit_callback(distance_callback)
|
863
|
+
|
864
|
+
routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
|
865
|
+
|
866
|
+
demand_callback = lambda do |from_index|
|
867
|
+
from_node = manager.index_to_node(from_index)
|
868
|
+
data[:demands][from_node]
|
869
|
+
end
|
870
|
+
|
871
|
+
demand_callback_index = routing.register_unary_transit_callback(demand_callback)
|
872
|
+
routing.add_dimension_with_vehicle_capacity(
|
873
|
+
demand_callback_index,
|
874
|
+
0, # null capacity slack
|
875
|
+
data[:vehicle_capacities], # vehicle maximum capacities
|
876
|
+
true, # start cumul to zero
|
877
|
+
"Capacity"
|
878
|
+
)
|
879
|
+
|
400
880
|
solution = routing.solve(first_solution_strategy: :path_cheapest_arc)
|
881
|
+
|
882
|
+
total_distance = 0
|
883
|
+
total_load = 0
|
884
|
+
data[:num_vehicles].times do |vehicle_id|
|
885
|
+
index = routing.start(vehicle_id)
|
886
|
+
plan_output = String.new("Route for vehicle #{vehicle_id}:\n")
|
887
|
+
route_distance = 0
|
888
|
+
route_load = 0
|
889
|
+
while !routing.end?(index)
|
890
|
+
node_index = manager.index_to_node(index)
|
891
|
+
route_load += data[:demands][node_index]
|
892
|
+
plan_output += " #{node_index} Load(#{route_load}) -> "
|
893
|
+
previous_index = index
|
894
|
+
index = solution.value(routing.next_var(index))
|
895
|
+
route_distance += routing.arc_cost_for_vehicle(previous_index, index, vehicle_id)
|
896
|
+
end
|
897
|
+
plan_output += " #{manager.index_to_node(index)} Load(#{route_load})\n"
|
898
|
+
plan_output += "Distance of the route: #{route_distance}m\n"
|
899
|
+
plan_output += "Load of the route: #{route_load}\n\n"
|
900
|
+
puts plan_output
|
901
|
+
total_distance += route_distance
|
902
|
+
total_load += route_load
|
903
|
+
end
|
904
|
+
puts "Total distance of all routes: #{total_distance}m"
|
905
|
+
puts "Total load of all routes: #{total_load}"
|
401
906
|
```
|
402
907
|
|
403
|
-
|
908
|
+
### Pickups and Deliveries
|
909
|
+
|
910
|
+
[Guide](https://developers.google.com/optimization/routing/pickup_delivery)
|
404
911
|
|
405
912
|
```ruby
|
406
|
-
|
913
|
+
data = {}
|
914
|
+
data[:distance_matrix] = [
|
915
|
+
[0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],
|
916
|
+
[548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210],
|
917
|
+
[776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754],
|
918
|
+
[696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358],
|
919
|
+
[582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244],
|
920
|
+
[274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708],
|
921
|
+
[502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480],
|
922
|
+
[194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856],
|
923
|
+
[308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514],
|
924
|
+
[194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468],
|
925
|
+
[536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354],
|
926
|
+
[502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844],
|
927
|
+
[388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730],
|
928
|
+
[354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536],
|
929
|
+
[468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194],
|
930
|
+
[776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798],
|
931
|
+
[662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0]
|
932
|
+
]
|
933
|
+
data[:pickups_deliveries] = [
|
934
|
+
[1, 6],
|
935
|
+
[2, 10],
|
936
|
+
[4, 3],
|
937
|
+
[5, 9],
|
938
|
+
[7, 8],
|
939
|
+
[15, 11],
|
940
|
+
[13, 12],
|
941
|
+
[16, 14],
|
942
|
+
]
|
943
|
+
data[:num_vehicles] = 4
|
944
|
+
data[:depot] = 0
|
945
|
+
|
946
|
+
manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].size, data[:num_vehicles], data[:depot])
|
947
|
+
routing = ORTools::RoutingModel.new(manager)
|
948
|
+
|
949
|
+
distance_callback = lambda do |from_index, to_index|
|
950
|
+
from_node = manager.index_to_node(from_index)
|
951
|
+
to_node = manager.index_to_node(to_index)
|
952
|
+
data[:distance_matrix][from_node][to_node]
|
953
|
+
end
|
954
|
+
|
955
|
+
transit_callback_index = routing.register_transit_callback(distance_callback)
|
956
|
+
routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
|
957
|
+
|
958
|
+
dimension_name = "Distance"
|
959
|
+
routing.add_dimension(
|
960
|
+
transit_callback_index,
|
961
|
+
0, # no slack
|
962
|
+
3000, # vehicle maximum travel distance
|
963
|
+
true, # start cumul to zero
|
964
|
+
dimension_name
|
965
|
+
)
|
966
|
+
distance_dimension = routing.mutable_dimension(dimension_name)
|
967
|
+
distance_dimension.global_span_cost_coefficient = 100
|
968
|
+
|
969
|
+
data[:pickups_deliveries].each do |request|
|
970
|
+
pickup_index = manager.node_to_index(request[0])
|
971
|
+
delivery_index = manager.node_to_index(request[1])
|
972
|
+
routing.add_pickup_and_delivery(pickup_index, delivery_index)
|
973
|
+
routing.solver.add(routing.vehicle_var(pickup_index) == routing.vehicle_var(delivery_index))
|
974
|
+
routing.solver.add(distance_dimension.cumul_var(pickup_index) <= distance_dimension.cumul_var(delivery_index))
|
975
|
+
end
|
976
|
+
|
977
|
+
solution = routing.solve(first_solution_strategy: :parallel_cheapest_insertion)
|
978
|
+
|
979
|
+
total_distance = 0
|
407
980
|
data[:num_vehicles].times do |vehicle_id|
|
408
981
|
index = routing.start(vehicle_id)
|
409
982
|
plan_output = String.new("Route for vehicle #{vehicle_id}:\n")
|
@@ -417,9 +990,341 @@ data[:num_vehicles].times do |vehicle_id|
|
|
417
990
|
plan_output += "#{manager.index_to_node(index)}\n"
|
418
991
|
plan_output += "Distance of the route: #{route_distance}m\n\n"
|
419
992
|
puts plan_output
|
420
|
-
|
993
|
+
total_distance += route_distance
|
421
994
|
end
|
422
|
-
puts "
|
995
|
+
puts "Total Distance of all routes: #{total_distance}m"
|
996
|
+
```
|
997
|
+
|
998
|
+
### Time Window Constraints
|
999
|
+
|
1000
|
+
[Guide](https://developers.google.com/optimization/routing/vrptw)
|
1001
|
+
|
1002
|
+
```ruby
|
1003
|
+
data = {}
|
1004
|
+
data[:time_matrix] = [
|
1005
|
+
[0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7],
|
1006
|
+
[6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14],
|
1007
|
+
[9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9],
|
1008
|
+
[8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16],
|
1009
|
+
[7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14],
|
1010
|
+
[3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8],
|
1011
|
+
[6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5],
|
1012
|
+
[2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10],
|
1013
|
+
[3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6],
|
1014
|
+
[2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5],
|
1015
|
+
[6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4],
|
1016
|
+
[6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10],
|
1017
|
+
[4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8],
|
1018
|
+
[4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6],
|
1019
|
+
[5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2],
|
1020
|
+
[9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9],
|
1021
|
+
[7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0],
|
1022
|
+
]
|
1023
|
+
data[:time_windows] = [
|
1024
|
+
[0, 5], # depot
|
1025
|
+
[7, 12], # 1
|
1026
|
+
[10, 15], # 2
|
1027
|
+
[16, 18], # 3
|
1028
|
+
[10, 13], # 4
|
1029
|
+
[0, 5], # 5
|
1030
|
+
[5, 10], # 6
|
1031
|
+
[0, 4], # 7
|
1032
|
+
[5, 10], # 8
|
1033
|
+
[0, 3], # 9
|
1034
|
+
[10, 16], # 10
|
1035
|
+
[10, 15], # 11
|
1036
|
+
[0, 5], # 12
|
1037
|
+
[5, 10], # 13
|
1038
|
+
[7, 8], # 14
|
1039
|
+
[10, 15], # 15
|
1040
|
+
[11, 15], # 16
|
1041
|
+
]
|
1042
|
+
data[:num_vehicles] = 4
|
1043
|
+
data[:depot] = 0
|
1044
|
+
|
1045
|
+
manager = ORTools::RoutingIndexManager.new(data[:time_matrix].size, data[:num_vehicles], data[:depot])
|
1046
|
+
routing = ORTools::RoutingModel.new(manager)
|
1047
|
+
|
1048
|
+
time_callback = lambda do |from_index, to_index|
|
1049
|
+
from_node = manager.index_to_node(from_index)
|
1050
|
+
to_node = manager.index_to_node(to_index)
|
1051
|
+
data[:time_matrix][from_node][to_node]
|
1052
|
+
end
|
1053
|
+
|
1054
|
+
transit_callback_index = routing.register_transit_callback(time_callback)
|
1055
|
+
routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
|
1056
|
+
time = "Time"
|
1057
|
+
routing.add_dimension(
|
1058
|
+
transit_callback_index,
|
1059
|
+
30, # allow waiting time
|
1060
|
+
30, # maximum time per vehicle
|
1061
|
+
false, # don't force start cumul to zero
|
1062
|
+
time
|
1063
|
+
)
|
1064
|
+
time_dimension = routing.mutable_dimension(time)
|
1065
|
+
|
1066
|
+
data[:time_windows].each_with_index do |time_window, location_idx|
|
1067
|
+
next if location_idx == 0
|
1068
|
+
index = manager.node_to_index(location_idx)
|
1069
|
+
time_dimension.cumul_var(index).set_range(time_window[0], time_window[1])
|
1070
|
+
end
|
1071
|
+
|
1072
|
+
data[:num_vehicles].times do |vehicle_id|
|
1073
|
+
index = routing.start(vehicle_id)
|
1074
|
+
time_dimension.cumul_var(index).set_range(data[:time_windows][0][0], data[:time_windows][0][1])
|
1075
|
+
end
|
1076
|
+
|
1077
|
+
data[:num_vehicles].times do |i|
|
1078
|
+
routing.add_variable_minimized_by_finalizer(time_dimension.cumul_var(routing.start(i)))
|
1079
|
+
routing.add_variable_minimized_by_finalizer(time_dimension.cumul_var(routing.end(i)))
|
1080
|
+
end
|
1081
|
+
|
1082
|
+
solution = routing.solve(first_solution_strategy: :path_cheapest_arc)
|
1083
|
+
|
1084
|
+
time_dimension = routing.mutable_dimension("Time")
|
1085
|
+
total_time = 0
|
1086
|
+
data[:num_vehicles].times do |vehicle_id|
|
1087
|
+
index = routing.start(vehicle_id)
|
1088
|
+
plan_output = String.new("Route for vehicle #{vehicle_id}:\n")
|
1089
|
+
while !routing.end?(index)
|
1090
|
+
time_var = time_dimension.cumul_var(index)
|
1091
|
+
plan_output += "#{manager.index_to_node(index)} Time(#{solution.min(time_var)},#{solution.max(time_var)}) -> "
|
1092
|
+
index = solution.value(routing.next_var(index))
|
1093
|
+
end
|
1094
|
+
time_var = time_dimension.cumul_var(index)
|
1095
|
+
plan_output += "#{manager.index_to_node(index)} Time(#{solution.min(time_var)},#{solution.max(time_var)})\n"
|
1096
|
+
plan_output += "Time of the route: #{solution.min(time_var)}min\n\n"
|
1097
|
+
puts plan_output
|
1098
|
+
total_time += solution.min(time_var)
|
1099
|
+
end
|
1100
|
+
puts "Total time of all routes: #{total_time}min"
|
1101
|
+
```
|
1102
|
+
|
1103
|
+
### Resource Constraints
|
1104
|
+
|
1105
|
+
[Guide](https://developers.google.com/optimization/routing/cvrptw_resources)
|
1106
|
+
|
1107
|
+
```ruby
|
1108
|
+
data = {}
|
1109
|
+
data[:time_matrix] = [
|
1110
|
+
[0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7],
|
1111
|
+
[6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14],
|
1112
|
+
[9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9],
|
1113
|
+
[8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16],
|
1114
|
+
[7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14],
|
1115
|
+
[3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8],
|
1116
|
+
[6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5],
|
1117
|
+
[2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10],
|
1118
|
+
[3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6],
|
1119
|
+
[2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5],
|
1120
|
+
[6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4],
|
1121
|
+
[6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10],
|
1122
|
+
[4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8],
|
1123
|
+
[4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6],
|
1124
|
+
[5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2],
|
1125
|
+
[9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9],
|
1126
|
+
[7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0]
|
1127
|
+
]
|
1128
|
+
data[:time_windows] = [
|
1129
|
+
[0, 5], # depot
|
1130
|
+
[7, 12], # 1
|
1131
|
+
[10, 15], # 2
|
1132
|
+
[5, 14], # 3
|
1133
|
+
[5, 13], # 4
|
1134
|
+
[0, 5], # 5
|
1135
|
+
[5, 10], # 6
|
1136
|
+
[0, 10], # 7
|
1137
|
+
[5, 10], # 8
|
1138
|
+
[0, 5], # 9
|
1139
|
+
[10, 16], # 10
|
1140
|
+
[10, 15], # 11
|
1141
|
+
[0, 5], # 12
|
1142
|
+
[5, 10], # 13
|
1143
|
+
[7, 12], # 14
|
1144
|
+
[10, 15], # 15
|
1145
|
+
[5, 15], # 16
|
1146
|
+
]
|
1147
|
+
data[:num_vehicles] = 4
|
1148
|
+
data[:vehicle_load_time] = 5
|
1149
|
+
data[:vehicle_unload_time] = 5
|
1150
|
+
data[:depot_capacity] = 2
|
1151
|
+
data[:depot] = 0
|
1152
|
+
|
1153
|
+
manager = ORTools::RoutingIndexManager.new(data[:time_matrix].size, data[:num_vehicles], data[:depot])
|
1154
|
+
routing = ORTools::RoutingModel.new(manager)
|
1155
|
+
|
1156
|
+
time_callback = lambda do |from_index, to_index|
|
1157
|
+
from_node = manager.index_to_node(from_index)
|
1158
|
+
to_node = manager.index_to_node(to_index)
|
1159
|
+
data[:time_matrix][from_node][to_node]
|
1160
|
+
end
|
1161
|
+
|
1162
|
+
transit_callback_index = routing.register_transit_callback(time_callback)
|
1163
|
+
|
1164
|
+
routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
|
1165
|
+
|
1166
|
+
time = "Time"
|
1167
|
+
routing.add_dimension(
|
1168
|
+
transit_callback_index,
|
1169
|
+
60, # allow waiting time
|
1170
|
+
60, # maximum time per vehicle
|
1171
|
+
false, # don't force start cumul to zero
|
1172
|
+
time
|
1173
|
+
)
|
1174
|
+
time_dimension = routing.mutable_dimension(time)
|
1175
|
+
data[:time_windows].each_with_index do |time_window, location_idx|
|
1176
|
+
next if location_idx == 0
|
1177
|
+
index = manager.node_to_index(location_idx)
|
1178
|
+
time_dimension.cumul_var(index).set_range(time_window[0], time_window[1])
|
1179
|
+
end
|
1180
|
+
|
1181
|
+
data[:num_vehicles].times do |vehicle_id|
|
1182
|
+
index = routing.start(vehicle_id)
|
1183
|
+
time_dimension.cumul_var(index).set_range(data[:time_windows][0][0], data[:time_windows][0][1])
|
1184
|
+
end
|
1185
|
+
|
1186
|
+
solver = routing.solver
|
1187
|
+
intervals = []
|
1188
|
+
data[:num_vehicles].times do |i|
|
1189
|
+
intervals << solver.fixed_duration_interval_var(
|
1190
|
+
time_dimension.cumul_var(routing.start(i)),
|
1191
|
+
data[:vehicle_load_time],
|
1192
|
+
"depot_interval"
|
1193
|
+
)
|
1194
|
+
intervals << solver.fixed_duration_interval_var(
|
1195
|
+
time_dimension.cumul_var(routing.end(i)),
|
1196
|
+
data[:vehicle_unload_time],
|
1197
|
+
"depot_interval"
|
1198
|
+
)
|
1199
|
+
end
|
1200
|
+
|
1201
|
+
depot_usage = [1] * intervals.size
|
1202
|
+
solver.add(solver.cumulative(intervals, depot_usage, data[:depot_capacity], "depot"))
|
1203
|
+
|
1204
|
+
data[:num_vehicles].times do |i|
|
1205
|
+
routing.add_variable_minimized_by_finalizer(time_dimension.cumul_var(routing.start(i)))
|
1206
|
+
routing.add_variable_minimized_by_finalizer(time_dimension.cumul_var(routing.end(i)))
|
1207
|
+
end
|
1208
|
+
|
1209
|
+
solution = routing.solve(first_solution_strategy: :path_cheapest_arc)
|
1210
|
+
|
1211
|
+
time_dimension = routing.mutable_dimension("Time")
|
1212
|
+
total_time = 0
|
1213
|
+
data[:num_vehicles].times do |vehicle_id|
|
1214
|
+
index = routing.start(vehicle_id)
|
1215
|
+
plan_output = String.new("Route for vehicle #{vehicle_id}:\n")
|
1216
|
+
while !routing.end?(index)
|
1217
|
+
time_var = time_dimension.cumul_var(index)
|
1218
|
+
plan_output += "#{manager.index_to_node(index)} Time(#{solution.min(time_var)},#{solution.max(time_var)}) -> "
|
1219
|
+
index = solution.value(routing.next_var(index))
|
1220
|
+
end
|
1221
|
+
time_var = time_dimension.cumul_var(index)
|
1222
|
+
plan_output += "#{manager.index_to_node(index)} Time(#{solution.min(time_var)},#{solution.max(time_var)})\n"
|
1223
|
+
plan_output += "Time of the route: #{solution.min(time_var)}min\n\n"
|
1224
|
+
puts plan_output
|
1225
|
+
total_time += solution.min(time_var)
|
1226
|
+
end
|
1227
|
+
puts "Total time of all routes: #{total_time}min"
|
1228
|
+
```
|
1229
|
+
|
1230
|
+
### Penalties and Dropping Visits
|
1231
|
+
|
1232
|
+
[Guide](https://developers.google.com/optimization/routing/penalties)
|
1233
|
+
|
1234
|
+
```ruby
|
1235
|
+
data = {}
|
1236
|
+
data[:distance_matrix] = [
|
1237
|
+
[0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],
|
1238
|
+
[548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210],
|
1239
|
+
[776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754],
|
1240
|
+
[696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358],
|
1241
|
+
[582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244],
|
1242
|
+
[274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708],
|
1243
|
+
[502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480],
|
1244
|
+
[194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856],
|
1245
|
+
[308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514],
|
1246
|
+
[194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468],
|
1247
|
+
[536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354],
|
1248
|
+
[502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844],
|
1249
|
+
[388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730],
|
1250
|
+
[354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536],
|
1251
|
+
[468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194],
|
1252
|
+
[776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798],
|
1253
|
+
[662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0]
|
1254
|
+
]
|
1255
|
+
data[:demands] = [0, 1, 1, 3, 6, 3, 6, 8, 8, 1, 2, 1, 2, 6, 6, 8, 8]
|
1256
|
+
data[:vehicle_capacities] = [15, 15, 15, 15]
|
1257
|
+
data[:num_vehicles] = 4
|
1258
|
+
data[:depot] = 0
|
1259
|
+
|
1260
|
+
manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].size, data[:num_vehicles], data[:depot])
|
1261
|
+
routing = ORTools::RoutingModel.new(manager)
|
1262
|
+
|
1263
|
+
distance_callback = lambda do |from_index, to_index|
|
1264
|
+
from_node = manager.index_to_node(from_index)
|
1265
|
+
to_node = manager.index_to_node(to_index)
|
1266
|
+
data[:distance_matrix][from_node][to_node]
|
1267
|
+
end
|
1268
|
+
|
1269
|
+
transit_callback_index = routing.register_transit_callback(distance_callback)
|
1270
|
+
|
1271
|
+
routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
|
1272
|
+
|
1273
|
+
demand_callback = lambda do |from_index|
|
1274
|
+
from_node = manager.index_to_node(from_index)
|
1275
|
+
data[:demands][from_node]
|
1276
|
+
end
|
1277
|
+
|
1278
|
+
demand_callback_index = routing.register_unary_transit_callback(demand_callback)
|
1279
|
+
routing.add_dimension_with_vehicle_capacity(
|
1280
|
+
demand_callback_index,
|
1281
|
+
0, # null capacity slack
|
1282
|
+
data[:vehicle_capacities], # vehicle maximum capacities
|
1283
|
+
true, # start cumul to zero
|
1284
|
+
"Capacity"
|
1285
|
+
)
|
1286
|
+
|
1287
|
+
penalty = 1000
|
1288
|
+
1.upto(data[:distance_matrix].size - 1) do |node|
|
1289
|
+
routing.add_disjunction([manager.node_to_index(node)], penalty)
|
1290
|
+
end
|
1291
|
+
|
1292
|
+
assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
|
1293
|
+
|
1294
|
+
dropped_nodes = String.new("Dropped nodes:")
|
1295
|
+
routing.size.times do |node|
|
1296
|
+
next if routing.start?(node) || routing.end?(node)
|
1297
|
+
|
1298
|
+
if assignment.value(routing.next_var(node)) == node
|
1299
|
+
dropped_nodes += " #{manager.index_to_node(node)}"
|
1300
|
+
end
|
1301
|
+
end
|
1302
|
+
puts dropped_nodes
|
1303
|
+
|
1304
|
+
total_distance = 0
|
1305
|
+
total_load = 0
|
1306
|
+
data[:num_vehicles].times do |vehicle_id|
|
1307
|
+
index = routing.start(vehicle_id)
|
1308
|
+
plan_output = "Route for vehicle #{vehicle_id}:\n"
|
1309
|
+
route_distance = 0
|
1310
|
+
route_load = 0
|
1311
|
+
while !routing.end?(index)
|
1312
|
+
node_index = manager.index_to_node(index)
|
1313
|
+
route_load += data[:demands][node_index]
|
1314
|
+
plan_output += " #{node_index} Load(#{route_load}) -> "
|
1315
|
+
previous_index = index
|
1316
|
+
index = assignment.value(routing.next_var(index))
|
1317
|
+
route_distance += routing.arc_cost_for_vehicle(previous_index, index, vehicle_id)
|
1318
|
+
end
|
1319
|
+
plan_output += " #{manager.index_to_node(index)} Load(#{route_load})\n"
|
1320
|
+
plan_output += "Distance of the route: #{route_distance}m\n"
|
1321
|
+
plan_output += "Load of the route: #{route_load}\n\n"
|
1322
|
+
puts plan_output
|
1323
|
+
total_distance += route_distance
|
1324
|
+
total_load += route_load
|
1325
|
+
end
|
1326
|
+
puts "Total Distance of all routes: #{total_distance}m"
|
1327
|
+
puts "Total Load of all routes: #{total_load}"
|
423
1328
|
```
|
424
1329
|
|
425
1330
|
### Routing Options
|
@@ -441,9 +1346,8 @@ routing.solve(
|
|
441
1346
|
|
442
1347
|
[Guide](https://developers.google.com/optimization/bin/knapsack)
|
443
1348
|
|
444
|
-
Create the data
|
445
|
-
|
446
1349
|
```ruby
|
1350
|
+
# create the data
|
447
1351
|
values = [
|
448
1352
|
360, 83, 59, 130, 431, 67, 230, 52, 93, 125, 670, 892, 600, 38, 48, 147,
|
449
1353
|
78, 256, 63, 17, 120, 164, 432, 35, 92, 110, 22, 42, 50, 323, 514, 28,
|
@@ -456,17 +1360,11 @@ weights = [[
|
|
456
1360
|
3, 86, 66, 31, 65, 0, 79, 20, 65, 52, 13
|
457
1361
|
]]
|
458
1362
|
capacities = [850]
|
459
|
-
```
|
460
|
-
|
461
|
-
Declare the solver
|
462
1363
|
|
463
|
-
|
1364
|
+
# declare the solver
|
464
1365
|
solver = ORTools::KnapsackSolver.new(:branch_and_bound, "KnapsackExample")
|
465
|
-
```
|
466
1366
|
|
467
|
-
|
468
|
-
|
469
|
-
```ruby
|
1367
|
+
# call the solver
|
470
1368
|
solver.init(values, weights, capacities)
|
471
1369
|
computed_value = solver.solve
|
472
1370
|
|
@@ -490,9 +1388,8 @@ puts "Packed weights: #{packed_weights}"
|
|
490
1388
|
|
491
1389
|
[Guide](https://developers.google.com/optimization/bin/multiple_knapsack)
|
492
1390
|
|
493
|
-
Create the data
|
494
|
-
|
495
1391
|
```ruby
|
1392
|
+
# create the data
|
496
1393
|
data = {}
|
497
1394
|
weights = [48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36]
|
498
1395
|
values = [10, 30, 25, 50, 35, 30, 15, 40, 30, 35, 45, 10, 20, 30, 25]
|
@@ -503,28 +1400,21 @@ data[:num_items] = weights.length
|
|
503
1400
|
num_bins = 5
|
504
1401
|
data[:bins] = (0...num_bins).to_a
|
505
1402
|
data[:bin_capacities] = [100, 100, 100, 100, 100]
|
506
|
-
```
|
507
1403
|
|
508
|
-
|
509
|
-
|
510
|
-
```ruby
|
1404
|
+
# declare the solver
|
511
1405
|
solver = ORTools::Solver.new("simple_mip_program", :cbc)
|
512
|
-
```
|
513
|
-
|
514
|
-
Create the variables
|
515
1406
|
|
516
|
-
|
1407
|
+
# create the variables
|
1408
|
+
# x[i, j] = 1 if item i is packed in bin j
|
517
1409
|
x = {}
|
518
1410
|
data[:items].each do |i|
|
519
1411
|
data[:bins].each do |j|
|
520
1412
|
x[[i, j]] = solver.int_var(0, 1, "x_%i_%i" % [i, j])
|
521
1413
|
end
|
522
1414
|
end
|
523
|
-
```
|
524
1415
|
|
525
|
-
|
526
|
-
|
527
|
-
```ruby
|
1416
|
+
# define the constraints
|
1417
|
+
# each item can be in at most one bin
|
528
1418
|
data[:items].each do |i|
|
529
1419
|
sum = ORTools::LinearExpr.new
|
530
1420
|
data[:bins].each do |j|
|
@@ -533,6 +1423,7 @@ data[:items].each do |i|
|
|
533
1423
|
solver.add(sum <= 1.0)
|
534
1424
|
end
|
535
1425
|
|
1426
|
+
# the amount packed in each bin cannot exceed its capacity
|
536
1427
|
data[:bins].each do |j|
|
537
1428
|
weight = ORTools::LinearExpr.new
|
538
1429
|
data[:items].each do |i|
|
@@ -540,11 +1431,8 @@ data[:bins].each do |j|
|
|
540
1431
|
end
|
541
1432
|
solver.add(weight <= data[:bin_capacities][j])
|
542
1433
|
end
|
543
|
-
```
|
544
|
-
|
545
|
-
Define the objective
|
546
1434
|
|
547
|
-
|
1435
|
+
# define the objective
|
548
1436
|
objective = solver.objective
|
549
1437
|
|
550
1438
|
data[:items].each do |i|
|
@@ -553,11 +1441,8 @@ data[:items].each do |i|
|
|
553
1441
|
end
|
554
1442
|
end
|
555
1443
|
objective.set_maximization
|
556
|
-
```
|
557
1444
|
|
558
|
-
|
559
|
-
|
560
|
-
```ruby
|
1445
|
+
# call the solver and print the solution
|
561
1446
|
status = solver.solve
|
562
1447
|
|
563
1448
|
if status == :optimal
|
@@ -589,26 +1474,20 @@ end
|
|
589
1474
|
|
590
1475
|
[Guide](https://developers.google.com/optimization/bin/bin_packing)
|
591
1476
|
|
592
|
-
Create the data
|
593
|
-
|
594
1477
|
```ruby
|
1478
|
+
# create the data
|
595
1479
|
data = {}
|
596
1480
|
weights = [48, 30, 19, 36, 36, 27, 42, 42, 36, 24, 30]
|
597
1481
|
data[:weights] = weights
|
598
1482
|
data[:items] = (0...weights.length).to_a
|
599
1483
|
data[:bins] = data[:items]
|
600
1484
|
data[:bin_capacity] = 100
|
601
|
-
```
|
602
1485
|
|
603
|
-
|
604
|
-
|
605
|
-
```ruby
|
1486
|
+
# create the mip solver with the CBC backend
|
606
1487
|
solver = ORTools::Solver.new("simple_mip_program", :cbc)
|
607
|
-
```
|
608
|
-
|
609
|
-
Create the variables
|
610
1488
|
|
611
|
-
|
1489
|
+
# variables
|
1490
|
+
# x[i, j] = 1 if item i is packed in bin j
|
612
1491
|
x = {}
|
613
1492
|
data[:items].each do |i|
|
614
1493
|
data[:bins].each do |j|
|
@@ -616,34 +1495,28 @@ data[:items].each do |i|
|
|
616
1495
|
end
|
617
1496
|
end
|
618
1497
|
|
1498
|
+
# y[j] = 1 if bin j is used
|
619
1499
|
y = {}
|
620
1500
|
data[:bins].each do |j|
|
621
1501
|
y[j] = solver.int_var(0, 1, "y[%i]" % j)
|
622
1502
|
end
|
623
|
-
```
|
624
1503
|
|
625
|
-
|
626
|
-
|
627
|
-
```ruby
|
1504
|
+
# constraints
|
1505
|
+
# each item must be in exactly one bin
|
628
1506
|
data[:items].each do |i|
|
629
1507
|
solver.add(solver.sum(data[:bins].map { |j| x[[i, j]] }) == 1)
|
630
1508
|
end
|
631
1509
|
|
1510
|
+
# the amount packed in each bin cannot exceed its capacity
|
632
1511
|
data[:bins].each do |j|
|
633
1512
|
sum = solver.sum(data[:items].map { |i| x[[i, j]] * data[:weights][i] })
|
634
1513
|
solver.add(sum <= y[j] * data[:bin_capacity])
|
635
1514
|
end
|
636
|
-
```
|
637
|
-
|
638
|
-
Define the objective
|
639
1515
|
|
640
|
-
|
1516
|
+
# objective: minimize the number of bins used
|
641
1517
|
solver.minimize(solver.sum(data[:bins].map { |j| y[j] }))
|
642
|
-
```
|
643
1518
|
|
644
|
-
|
645
|
-
|
646
|
-
```ruby
|
1519
|
+
# call the solver and print the solution
|
647
1520
|
if status == :optimal
|
648
1521
|
num_bins = 0
|
649
1522
|
data[:bins].each do |j|
|
@@ -677,27 +1550,20 @@ end
|
|
677
1550
|
|
678
1551
|
[Guide](https://developers.google.com/optimization/flow/maxflow)
|
679
1552
|
|
680
|
-
Define the data
|
681
|
-
|
682
1553
|
```ruby
|
1554
|
+
# define the data
|
683
1555
|
start_nodes = [0, 0, 0, 1, 1, 2, 2, 3, 3]
|
684
1556
|
end_nodes = [1, 2, 3, 2, 4, 3, 4, 2, 4]
|
685
1557
|
capacities = [20, 30, 10, 40, 30, 10, 20, 5, 20]
|
686
|
-
```
|
687
1558
|
|
688
|
-
|
689
|
-
|
690
|
-
```ruby
|
1559
|
+
# declare the solver and add the arcs
|
691
1560
|
max_flow = ORTools::SimpleMaxFlow.new
|
692
1561
|
|
693
1562
|
start_nodes.length.times do |i|
|
694
1563
|
max_flow.add_arc_with_capacity(start_nodes[i], end_nodes[i], capacities[i])
|
695
1564
|
end
|
696
|
-
```
|
697
|
-
|
698
|
-
Invoke the solver and display the results
|
699
1565
|
|
700
|
-
|
1566
|
+
# invoke the solver and display the results
|
701
1567
|
if max_flow.solve(0, 4) == :optimal
|
702
1568
|
puts "Max flow: #{max_flow.optimal_flow}"
|
703
1569
|
puts
|
@@ -721,122 +1587,56 @@ end
|
|
721
1587
|
|
722
1588
|
[Guide](https://developers.google.com/optimization/flow/mincostflow)
|
723
1589
|
|
724
|
-
Define the data
|
725
|
-
|
726
1590
|
```ruby
|
1591
|
+
# define the data
|
727
1592
|
start_nodes = [ 0, 0, 1, 1, 1, 2, 2, 3, 4]
|
728
1593
|
end_nodes = [ 1, 2, 2, 3, 4, 3, 4, 4, 2]
|
729
1594
|
capacities = [15, 8, 20, 4, 10, 15, 4, 20, 5]
|
730
1595
|
unit_costs = [ 4, 4, 2, 2, 6, 1, 3, 2, 3]
|
731
1596
|
supplies = [20, 0, 0, -5, -15]
|
732
|
-
```
|
733
|
-
|
734
|
-
Declare the solver and add the arcs
|
735
|
-
|
736
|
-
```ruby
|
737
|
-
min_cost_flow = ORTools::SimpleMinCostFlow.new
|
738
|
-
|
739
|
-
start_nodes.length.times do |i|
|
740
|
-
min_cost_flow.add_arc_with_capacity_and_unit_cost(
|
741
|
-
start_nodes[i], end_nodes[i], capacities[i], unit_costs[i]
|
742
|
-
)
|
743
|
-
end
|
744
|
-
|
745
|
-
supplies.length.times do |i|
|
746
|
-
min_cost_flow.set_node_supply(i, supplies[i])
|
747
|
-
end
|
748
|
-
```
|
749
1597
|
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
min_cost_flow.num_arcs.times do |i|
|
758
|
-
cost = min_cost_flow.flow(i) * min_cost_flow.unit_cost(i)
|
759
|
-
puts "%1s -> %1s %3s / %3s %3s" % [
|
760
|
-
min_cost_flow.tail(i),
|
761
|
-
min_cost_flow.head(i),
|
762
|
-
min_cost_flow.flow(i),
|
763
|
-
min_cost_flow.capacity(i),
|
764
|
-
cost
|
765
|
-
]
|
766
|
-
end
|
767
|
-
else
|
768
|
-
puts "There was an issue with the min cost flow input."
|
769
|
-
end
|
770
|
-
```
|
771
|
-
|
772
|
-
## Assignment
|
773
|
-
|
774
|
-
[Guide](https://developers.google.com/optimization/assignment/simple_assignment)
|
775
|
-
|
776
|
-
Create the data
|
777
|
-
|
778
|
-
```ruby
|
779
|
-
cost = [[ 90, 76, 75, 70],
|
780
|
-
[ 35, 85, 55, 65],
|
781
|
-
[125, 95, 90, 105],
|
782
|
-
[ 45, 110, 95, 115]]
|
783
|
-
|
784
|
-
rows = cost.length
|
785
|
-
cols = cost[0].length
|
786
|
-
```
|
787
|
-
|
788
|
-
Create the solver
|
789
|
-
|
790
|
-
```ruby
|
791
|
-
assignment = ORTools::LinearSumAssignment.new
|
792
|
-
```
|
793
|
-
|
794
|
-
Add the costs to the solver
|
795
|
-
|
796
|
-
```ruby
|
797
|
-
rows.times do |worker|
|
798
|
-
cols.times do |task|
|
799
|
-
if cost[worker][task]
|
800
|
-
assignment.add_arc_with_cost(worker, task, cost[worker][task])
|
801
|
-
end
|
802
|
-
end
|
1598
|
+
# declare the solver and add the arcs
|
1599
|
+
min_cost_flow = ORTools::SimpleMinCostFlow.new
|
1600
|
+
|
1601
|
+
start_nodes.length.times do |i|
|
1602
|
+
min_cost_flow.add_arc_with_capacity_and_unit_cost(
|
1603
|
+
start_nodes[i], end_nodes[i], capacities[i], unit_costs[i]
|
1604
|
+
)
|
803
1605
|
end
|
804
|
-
```
|
805
1606
|
|
806
|
-
|
1607
|
+
supplies.length.times do |i|
|
1608
|
+
min_cost_flow.set_node_supply(i, supplies[i])
|
1609
|
+
end
|
807
1610
|
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
puts "Total cost = #{assignment.optimal_cost}"
|
1611
|
+
# invoke the solver and display the results
|
1612
|
+
if min_cost_flow.solve == :optimal
|
1613
|
+
puts "Minimum cost #{min_cost_flow.optimal_cost}"
|
812
1614
|
puts
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
1615
|
+
puts " Arc Flow / Capacity Cost"
|
1616
|
+
min_cost_flow.num_arcs.times do |i|
|
1617
|
+
cost = min_cost_flow.flow(i) * min_cost_flow.unit_cost(i)
|
1618
|
+
puts "%1s -> %1s %3s / %3s %3s" % [
|
1619
|
+
min_cost_flow.tail(i),
|
1620
|
+
min_cost_flow.head(i),
|
1621
|
+
min_cost_flow.flow(i),
|
1622
|
+
min_cost_flow.capacity(i),
|
1623
|
+
cost
|
818
1624
|
]
|
819
1625
|
end
|
820
|
-
|
821
|
-
puts "
|
822
|
-
elsif solve_status == :possible_overflow
|
823
|
-
puts "Some input costs are too large and may cause an integer overflow."
|
1626
|
+
else
|
1627
|
+
puts "There was an issue with the min cost flow input."
|
824
1628
|
end
|
825
1629
|
```
|
826
1630
|
|
827
|
-
|
1631
|
+
### Assignment as a Min Cost Flow Problem
|
828
1632
|
|
829
1633
|
[Guide](https://developers.google.com/optimization/assignment/assignment_min_cost_flow)
|
830
1634
|
|
831
|
-
Create the solver
|
832
|
-
|
833
1635
|
```ruby
|
1636
|
+
# create the solver
|
834
1637
|
min_cost_flow = ORTools::SimpleMinCostFlow.new
|
835
|
-
```
|
836
1638
|
|
837
|
-
|
838
|
-
|
839
|
-
```ruby
|
1639
|
+
# create the data
|
840
1640
|
start_nodes = [0, 0, 0, 0] + [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4] + [5, 6, 7, 8]
|
841
1641
|
end_nodes = [1, 2, 3, 4] + [5, 6, 7, 8, 5, 6, 7, 8, 5, 6, 7, 8, 5, 6, 7, 8] + [9, 9, 9, 9]
|
842
1642
|
capacities = [1, 1, 1, 1] + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + [1, 1, 1, 1]
|
@@ -845,11 +1645,8 @@ supplies = [4, 0, 0, 0, 0, 0, 0, 0, 0, -4]
|
|
845
1645
|
source = 0
|
846
1646
|
sink = 9
|
847
1647
|
tasks = 4
|
848
|
-
```
|
849
|
-
|
850
|
-
Create the graph and constraints
|
851
1648
|
|
852
|
-
|
1649
|
+
# create the graph and constraints
|
853
1650
|
start_nodes.length.times do |i|
|
854
1651
|
min_cost_flow.add_arc_with_capacity_and_unit_cost(
|
855
1652
|
start_nodes[i], end_nodes[i], capacities[i], costs[i]
|
@@ -859,11 +1656,8 @@ end
|
|
859
1656
|
supplies.length.times do |i|
|
860
1657
|
min_cost_flow.set_node_supply(i, supplies[i])
|
861
1658
|
end
|
862
|
-
```
|
863
1659
|
|
864
|
-
|
865
|
-
|
866
|
-
```ruby
|
1660
|
+
# invoke the solver
|
867
1661
|
if min_cost_flow.solve == :optimal
|
868
1662
|
puts "Total cost = #{min_cost_flow.optimal_cost}"
|
869
1663
|
puts
|
@@ -883,109 +1677,20 @@ else
|
|
883
1677
|
end
|
884
1678
|
```
|
885
1679
|
|
886
|
-
|
887
|
-
|
888
|
-
[Guide](https://developers.google.com/optimization/assignment/assignment_mip)
|
889
|
-
|
890
|
-
Create the solver
|
891
|
-
|
892
|
-
```ruby
|
893
|
-
solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
|
894
|
-
```
|
895
|
-
|
896
|
-
Create the data
|
897
|
-
|
898
|
-
```ruby
|
899
|
-
cost = [[90, 76, 75, 70],
|
900
|
-
[35, 85, 55, 65],
|
901
|
-
[125, 95, 90, 105],
|
902
|
-
[45, 110, 95, 115],
|
903
|
-
[60, 105, 80, 75],
|
904
|
-
[45, 65, 110, 95]]
|
905
|
-
|
906
|
-
team1 = [0, 2, 4]
|
907
|
-
team2 = [1, 3, 5]
|
908
|
-
team_max = 2
|
909
|
-
```
|
910
|
-
|
911
|
-
Create the variables
|
912
|
-
|
913
|
-
```ruby
|
914
|
-
num_workers = cost.length
|
915
|
-
num_tasks = cost[1].length
|
916
|
-
x = {}
|
917
|
-
|
918
|
-
num_workers.times do |i|
|
919
|
-
num_tasks.times do |j|
|
920
|
-
x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
|
921
|
-
end
|
922
|
-
end
|
923
|
-
```
|
924
|
-
|
925
|
-
Create the objective function
|
926
|
-
|
927
|
-
```ruby
|
928
|
-
solver.minimize(solver.sum(
|
929
|
-
num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
|
930
|
-
))
|
931
|
-
```
|
932
|
-
|
933
|
-
Create the constraints
|
934
|
-
|
935
|
-
```ruby
|
936
|
-
num_workers.times do |i|
|
937
|
-
solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
|
938
|
-
end
|
939
|
-
|
940
|
-
num_tasks.times do |j|
|
941
|
-
solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
|
942
|
-
end
|
943
|
-
|
944
|
-
solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
|
945
|
-
solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
|
946
|
-
```
|
947
|
-
|
948
|
-
Invoke the solver
|
949
|
-
|
950
|
-
```ruby
|
951
|
-
sol = solver.solve
|
952
|
-
|
953
|
-
puts "Total cost = #{solver.objective.value}"
|
954
|
-
puts
|
955
|
-
num_workers.times do |i|
|
956
|
-
num_tasks.times do |j|
|
957
|
-
if x[[i, j]].solution_value > 0
|
958
|
-
puts "Worker %d assigned to task %d. Cost = %d" % [
|
959
|
-
i,
|
960
|
-
j,
|
961
|
-
cost[i][j]
|
962
|
-
]
|
963
|
-
end
|
964
|
-
end
|
965
|
-
end
|
966
|
-
|
967
|
-
puts
|
968
|
-
puts "Time = #{solver.wall_time} milliseconds"
|
969
|
-
```
|
970
|
-
|
971
|
-
## Employee Scheduling
|
1680
|
+
### Employee Scheduling
|
972
1681
|
|
973
1682
|
[Guide](https://developers.google.com/optimization/scheduling/employee_scheduling)
|
974
1683
|
|
975
|
-
Define the data
|
976
|
-
|
977
1684
|
```ruby
|
1685
|
+
# define the data
|
978
1686
|
num_nurses = 4
|
979
1687
|
num_shifts = 3
|
980
1688
|
num_days = 3
|
981
1689
|
all_nurses = num_nurses.times.to_a
|
982
1690
|
all_shifts = num_shifts.times.to_a
|
983
1691
|
all_days = num_days.times.to_a
|
984
|
-
```
|
985
|
-
|
986
|
-
Create the variables
|
987
1692
|
|
988
|
-
|
1693
|
+
# create the variables
|
989
1694
|
model = ORTools::CpModel.new
|
990
1695
|
|
991
1696
|
shifts = {}
|
@@ -996,11 +1701,8 @@ all_nurses.each do |n|
|
|
996
1701
|
end
|
997
1702
|
end
|
998
1703
|
end
|
999
|
-
```
|
1000
1704
|
|
1001
|
-
|
1002
|
-
|
1003
|
-
```ruby
|
1705
|
+
# assign nurses to shifts
|
1004
1706
|
all_days.each do |d|
|
1005
1707
|
all_shifts.each do |s|
|
1006
1708
|
model.add(model.sum(all_nurses.map { |n| shifts[[n, d, s]] }) == 1)
|
@@ -1012,11 +1714,8 @@ all_nurses.each do |n|
|
|
1012
1714
|
model.add(model.sum(all_shifts.map { |s| shifts[[n, d, s]] }) <= 1)
|
1013
1715
|
end
|
1014
1716
|
end
|
1015
|
-
```
|
1016
|
-
|
1017
|
-
Assign shifts evenly
|
1018
1717
|
|
1019
|
-
|
1718
|
+
# assign shifts evenly
|
1020
1719
|
min_shifts_per_nurse = (num_shifts * num_days) / num_nurses
|
1021
1720
|
max_shifts_per_nurse = min_shifts_per_nurse + 1
|
1022
1721
|
all_nurses.each do |n|
|
@@ -1024,11 +1723,8 @@ all_nurses.each do |n|
|
|
1024
1723
|
model.add(num_shifts_worked >= min_shifts_per_nurse)
|
1025
1724
|
model.add(num_shifts_worked <= max_shifts_per_nurse)
|
1026
1725
|
end
|
1027
|
-
```
|
1028
1726
|
|
1029
|
-
|
1030
|
-
|
1031
|
-
```ruby
|
1727
|
+
# create a printer
|
1032
1728
|
class NursesPartialSolutionPrinter < ORTools::CpSolverSolutionCallback
|
1033
1729
|
attr_reader :solution_count
|
1034
1730
|
|
@@ -1065,11 +1761,8 @@ class NursesPartialSolutionPrinter < ORTools::CpSolverSolutionCallback
|
|
1065
1761
|
@solution_count += 1
|
1066
1762
|
end
|
1067
1763
|
end
|
1068
|
-
```
|
1069
|
-
|
1070
|
-
Call the solver and display the results
|
1071
1764
|
|
1072
|
-
|
1765
|
+
# call the solver and display the results
|
1073
1766
|
solver = ORTools::CpSolver.new
|
1074
1767
|
a_few_solutions = 5.times.to_a
|
1075
1768
|
solution_printer = NursesPartialSolutionPrinter.new(
|
@@ -1085,6 +1778,395 @@ puts " - wall time : %f s" % solver.wall_time
|
|
1085
1778
|
puts " - solutions found : %i" % solution_printer.solution_count
|
1086
1779
|
```
|
1087
1780
|
|
1781
|
+
### The Job Shop Problem
|
1782
|
+
|
1783
|
+
[Guide](https://developers.google.com/optimization/scheduling/job_shop)
|
1784
|
+
|
1785
|
+
```ruby
|
1786
|
+
# create the model
|
1787
|
+
model = ORTools::CpModel.new
|
1788
|
+
|
1789
|
+
jobs_data = [
|
1790
|
+
[[0, 3], [1, 2], [2, 2]],
|
1791
|
+
[[0, 2], [2, 1], [1, 4]],
|
1792
|
+
[[1, 4], [2, 3]]
|
1793
|
+
]
|
1794
|
+
|
1795
|
+
machines_count = 1 + jobs_data.flat_map { |job| job.map { |task| task[0] } }.max
|
1796
|
+
all_machines = machines_count.times.to_a
|
1797
|
+
|
1798
|
+
# computes horizon dynamically as the sum of all durations
|
1799
|
+
horizon = jobs_data.flat_map { |job| job.map { |task| task[1] } }.sum
|
1800
|
+
|
1801
|
+
# creates job intervals and add to the corresponding machine lists
|
1802
|
+
all_tasks = {}
|
1803
|
+
machine_to_intervals = Hash.new { |hash, key| hash[key] = [] }
|
1804
|
+
|
1805
|
+
jobs_data.each_with_index do |job, job_id|
|
1806
|
+
job.each_with_index do |task, task_id|
|
1807
|
+
machine = task[0]
|
1808
|
+
duration = task[1]
|
1809
|
+
suffix = "_%i_%i" % [job_id, task_id]
|
1810
|
+
start_var = model.new_int_var(0, horizon, "start" + suffix)
|
1811
|
+
duration_var = model.new_int_var(duration, duration, "duration" + suffix)
|
1812
|
+
end_var = model.new_int_var(0, horizon, "end" + suffix)
|
1813
|
+
interval_var = model.new_interval_var(start_var, duration_var, end_var, "interval" + suffix)
|
1814
|
+
all_tasks[[job_id, task_id]] = {start: start_var, end: end_var, interval: interval_var}
|
1815
|
+
machine_to_intervals[machine] << interval_var
|
1816
|
+
end
|
1817
|
+
end
|
1818
|
+
|
1819
|
+
# create and add disjunctive constraints
|
1820
|
+
all_machines.each do |machine|
|
1821
|
+
model.add_no_overlap(machine_to_intervals[machine])
|
1822
|
+
end
|
1823
|
+
|
1824
|
+
# precedences inside a job
|
1825
|
+
jobs_data.each_with_index do |job, job_id|
|
1826
|
+
(job.size - 1).times do |task_id|
|
1827
|
+
model.add(all_tasks[[job_id, task_id + 1]][:start] >= all_tasks[[job_id, task_id]][:end])
|
1828
|
+
end
|
1829
|
+
end
|
1830
|
+
|
1831
|
+
# makespan objective
|
1832
|
+
obj_var = model.new_int_var(0, horizon, "makespan")
|
1833
|
+
model.add_max_equality(obj_var, jobs_data.map.with_index { |job, job_id| all_tasks[[job_id, job.size - 1]][:end] })
|
1834
|
+
model.minimize(obj_var)
|
1835
|
+
|
1836
|
+
# solve model
|
1837
|
+
solver = ORTools::CpSolver.new
|
1838
|
+
status = solver.solve(model)
|
1839
|
+
|
1840
|
+
# create one list of assigned tasks per machine
|
1841
|
+
assigned_jobs = Hash.new { |hash, key| hash[key] = [] }
|
1842
|
+
jobs_data.each_with_index do |job, job_id|
|
1843
|
+
job.each_with_index do |task, task_id|
|
1844
|
+
machine = task[0]
|
1845
|
+
assigned_jobs[machine] << {
|
1846
|
+
start: solver.value(all_tasks[[job_id, task_id]][:start]),
|
1847
|
+
job: job_id,
|
1848
|
+
index: task_id,
|
1849
|
+
duration: task[1]
|
1850
|
+
}
|
1851
|
+
end
|
1852
|
+
end
|
1853
|
+
|
1854
|
+
# create per machine output lines
|
1855
|
+
output = String.new("")
|
1856
|
+
all_machines.each do |machine|
|
1857
|
+
# sort by starting time
|
1858
|
+
assigned_jobs[machine].sort_by! { |v| v[:start] }
|
1859
|
+
sol_line_tasks = "Machine #{machine}: "
|
1860
|
+
sol_line = " "
|
1861
|
+
|
1862
|
+
assigned_jobs[machine].each do |assigned_task|
|
1863
|
+
name = "job_%i_%i" % [assigned_task[:job], assigned_task[:index]]
|
1864
|
+
# add spaces to output to align columns
|
1865
|
+
sol_line_tasks += "%-10s" % name
|
1866
|
+
start = assigned_task[:start]
|
1867
|
+
duration = assigned_task[:duration]
|
1868
|
+
sol_tmp = "[%i,%i]" % [start, start + duration]
|
1869
|
+
# add spaces to output to align columns
|
1870
|
+
sol_line += "%-10s" % sol_tmp
|
1871
|
+
end
|
1872
|
+
|
1873
|
+
sol_line += "\n"
|
1874
|
+
sol_line_tasks += "\n"
|
1875
|
+
output += sol_line_tasks
|
1876
|
+
output += sol_line
|
1877
|
+
end
|
1878
|
+
|
1879
|
+
# finally print the solution found
|
1880
|
+
puts "Optimal Schedule Length: %i" % solver.objective_value
|
1881
|
+
puts output
|
1882
|
+
```
|
1883
|
+
|
1884
|
+
### Sudoku
|
1885
|
+
|
1886
|
+
[Example](https://github.com/google/or-tools/blob/stable/examples/python/sudoku_sat.py)
|
1887
|
+
|
1888
|
+
```ruby
|
1889
|
+
# create the model
|
1890
|
+
model = ORTools::CpModel.new
|
1891
|
+
|
1892
|
+
cell_size = 3
|
1893
|
+
line_size = cell_size**2
|
1894
|
+
line = (0...line_size).to_a
|
1895
|
+
cell = (0...cell_size).to_a
|
1896
|
+
|
1897
|
+
initial_grid = [
|
1898
|
+
[0, 6, 0, 0, 5, 0, 0, 2, 0],
|
1899
|
+
[0, 0, 0, 3, 0, 0, 0, 9, 0],
|
1900
|
+
[7, 0, 0, 6, 0, 0, 0, 1, 0],
|
1901
|
+
[0, 0, 6, 0, 3, 0, 4, 0, 0],
|
1902
|
+
[0, 0, 4, 0, 7, 0, 1, 0, 0],
|
1903
|
+
[0, 0, 5, 0, 9, 0, 8, 0, 0],
|
1904
|
+
[0, 4, 0, 0, 0, 1, 0, 0, 6],
|
1905
|
+
[0, 3, 0, 0, 0, 8, 0, 0, 0],
|
1906
|
+
[0, 2, 0, 0, 4, 0, 0, 5, 0]
|
1907
|
+
]
|
1908
|
+
|
1909
|
+
grid = {}
|
1910
|
+
line.each do |i|
|
1911
|
+
line.each do |j|
|
1912
|
+
grid[[i, j]] = model.new_int_var(1, line_size, "grid %i %i" % [i, j])
|
1913
|
+
end
|
1914
|
+
end
|
1915
|
+
|
1916
|
+
# all different on rows
|
1917
|
+
line.each do |i|
|
1918
|
+
model.add_all_different(line.map { |j| grid[[i, j]] })
|
1919
|
+
end
|
1920
|
+
|
1921
|
+
# all different on columns
|
1922
|
+
line.each do |j|
|
1923
|
+
model.add_all_different(line.map { |i| grid[[i, j]] })
|
1924
|
+
end
|
1925
|
+
|
1926
|
+
# all different on cells
|
1927
|
+
cell.each do |i|
|
1928
|
+
cell.each do |j|
|
1929
|
+
one_cell = []
|
1930
|
+
cell.each do |di|
|
1931
|
+
cell.each do |dj|
|
1932
|
+
one_cell << grid[[i * cell_size + di, j * cell_size + dj]]
|
1933
|
+
end
|
1934
|
+
end
|
1935
|
+
model.add_all_different(one_cell)
|
1936
|
+
end
|
1937
|
+
end
|
1938
|
+
|
1939
|
+
# initial values
|
1940
|
+
line.each do |i|
|
1941
|
+
line.each do |j|
|
1942
|
+
if initial_grid[i][j] != 0
|
1943
|
+
model.add(grid[[i, j]] == initial_grid[i][j])
|
1944
|
+
end
|
1945
|
+
end
|
1946
|
+
end
|
1947
|
+
|
1948
|
+
# solve and print solution
|
1949
|
+
solver = ORTools::CpSolver.new
|
1950
|
+
status = solver.solve(model)
|
1951
|
+
if status == :optimal
|
1952
|
+
line.each do |i|
|
1953
|
+
p line.map { |j| solver.value(grid[[i, j]]) }
|
1954
|
+
end
|
1955
|
+
end
|
1956
|
+
```
|
1957
|
+
|
1958
|
+
### Wedding Seating Chart
|
1959
|
+
|
1960
|
+
[Example](https://github.com/google/or-tools/blob/stable/examples/python/wedding_optimal_chart_sat.py)
|
1961
|
+
|
1962
|
+
```ruby
|
1963
|
+
# From
|
1964
|
+
# Meghan L. Bellows and J. D. Luc Peterson
|
1965
|
+
# "Finding an optimal seating chart for a wedding"
|
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
|
1968
|
+
#
|
1969
|
+
# Every year, millions of brides (not to mention their mothers, future
|
1970
|
+
# mothers-in-law, and occasionally grooms) struggle with one of the
|
1971
|
+
# most daunting tasks during the wedding-planning process: the
|
1972
|
+
# seating chart. The guest responses are in, banquet hall is booked,
|
1973
|
+
# menu choices have been made. You think the hard parts are over,
|
1974
|
+
# but you have yet to embark upon the biggest headache of them all.
|
1975
|
+
# In order to make this process easier, we present a mathematical
|
1976
|
+
# formulation that models the seating chart problem. This model can
|
1977
|
+
# be solved to find the optimal arrangement of guests at tables.
|
1978
|
+
# At the very least, it can provide a starting point and hopefully
|
1979
|
+
# minimize stress and arguments.
|
1980
|
+
#
|
1981
|
+
# Adapted from
|
1982
|
+
# https://github.com/google/or-tools/blob/stable/examples/python/wedding_optimal_chart_sat.py
|
1983
|
+
|
1984
|
+
# Easy problem (from the paper)
|
1985
|
+
# num_tables = 2
|
1986
|
+
# table_capacity = 10
|
1987
|
+
# min_known_neighbors = 1
|
1988
|
+
|
1989
|
+
# Slightly harder problem (also from the paper)
|
1990
|
+
num_tables = 5
|
1991
|
+
table_capacity = 4
|
1992
|
+
min_known_neighbors = 1
|
1993
|
+
|
1994
|
+
# Connection matrix: who knows who, and how strong
|
1995
|
+
# is the relation
|
1996
|
+
c = [
|
1997
|
+
[1, 50, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
1998
|
+
[50, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
1999
|
+
[1, 1, 1, 50, 1, 1, 1, 1, 10, 0, 0, 0, 0, 0, 0, 0, 0],
|
2000
|
+
[1, 1, 50, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
2001
|
+
[1, 1, 1, 1, 1, 50, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
2002
|
+
[1, 1, 1, 1, 50, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
2003
|
+
[1, 1, 1, 1, 1, 1, 1, 50, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
2004
|
+
[1, 1, 1, 1, 1, 1, 50, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
2005
|
+
[1, 1, 10, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
2006
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 50, 1, 1, 1, 1, 1, 1],
|
2007
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 1, 1, 1, 1, 1, 1, 1],
|
2008
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
|
2009
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
|
2010
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
|
2011
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
|
2012
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
|
2013
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]
|
2014
|
+
]
|
2015
|
+
|
2016
|
+
# Names of the guests. B: Bride side, G: Groom side
|
2017
|
+
names = [
|
2018
|
+
"Deb (B)", "John (B)", "Martha (B)", "Travis (B)", "Allan (B)",
|
2019
|
+
"Lois (B)", "Jayne (B)", "Brad (B)", "Abby (B)", "Mary Helen (G)",
|
2020
|
+
"Lee (G)", "Annika (G)", "Carl (G)", "Colin (G)", "Shirley (G)",
|
2021
|
+
"DeAnn (G)", "Lori (G)"
|
2022
|
+
]
|
2023
|
+
|
2024
|
+
num_guests = c.size
|
2025
|
+
|
2026
|
+
all_tables = num_tables.times.to_a
|
2027
|
+
all_guests = num_guests.times.to_a
|
2028
|
+
|
2029
|
+
# create the cp model
|
2030
|
+
model = ORTools::CpModel.new
|
2031
|
+
|
2032
|
+
# decision variables
|
2033
|
+
seats = {}
|
2034
|
+
all_tables.each do |t|
|
2035
|
+
all_guests.each do |g|
|
2036
|
+
seats[[t, g]] = model.new_bool_var("guest %i seats on table %i" % [g, t])
|
2037
|
+
end
|
2038
|
+
end
|
2039
|
+
|
2040
|
+
pairs = all_guests.combination(2)
|
2041
|
+
|
2042
|
+
colocated = {}
|
2043
|
+
pairs.each do |g1, g2|
|
2044
|
+
colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
|
2045
|
+
end
|
2046
|
+
|
2047
|
+
same_table = {}
|
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])
|
2051
|
+
end
|
2052
|
+
end
|
2053
|
+
|
2054
|
+
# Objective
|
2055
|
+
model.maximize(model.sum((num_guests - 1).times.flat_map { |g1| (g1 + 1).upto(num_guests - 1).select { |g2| c[g1][g2] > 0 }.map { |g2| colocated[[g1, g2]] * c[g1][g2] } }))
|
2056
|
+
|
2057
|
+
#
|
2058
|
+
# Constraints
|
2059
|
+
#
|
2060
|
+
|
2061
|
+
# Everybody seats at one table.
|
2062
|
+
all_guests.each do |g|
|
2063
|
+
model.add(model.sum(all_tables.map { |t| seats[[t, g]] }) == 1)
|
2064
|
+
end
|
2065
|
+
|
2066
|
+
# Tables have a max capacity.
|
2067
|
+
all_tables.each do |t|
|
2068
|
+
model.add(model.sum(all_guests.map { |g| seats[[t, g]] }) <= table_capacity)
|
2069
|
+
end
|
2070
|
+
|
2071
|
+
# Link colocated with seats
|
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]])
|
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]])
|
2082
|
+
end
|
2083
|
+
|
2084
|
+
# Min known neighbors rule.
|
2085
|
+
all_guests.each do |g|
|
2086
|
+
model.add(
|
2087
|
+
model.sum(
|
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]] }
|
2098
|
+
) >= min_known_neighbors
|
2099
|
+
)
|
2100
|
+
end
|
2101
|
+
|
2102
|
+
# Symmetry breaking. First guest seats on the first table.
|
2103
|
+
model.add(seats[[0, 0]] == 1)
|
2104
|
+
|
2105
|
+
# Solve model
|
2106
|
+
solver = ORTools::CpSolver.new
|
2107
|
+
solution_printer = WeddingChartPrinter.new(seats, names, num_tables, num_guests)
|
2108
|
+
solver.solve_with_solution_callback(model, solution_printer)
|
2109
|
+
|
2110
|
+
puts "Statistics"
|
2111
|
+
puts " - conflicts : %i" % solver.num_conflicts
|
2112
|
+
puts " - branches : %i" % solver.num_branches
|
2113
|
+
puts " - wall time : %f s" % solver.wall_time
|
2114
|
+
puts " - num solutions: %i" % solution_printer.num_solutions
|
2115
|
+
```
|
2116
|
+
|
2117
|
+
### Set Partitioning
|
2118
|
+
|
2119
|
+
[Example](https://pythonhosted.org/PuLP/CaseStudies/a_set_partitioning_problem.html)
|
2120
|
+
|
2121
|
+
```ruby
|
2122
|
+
# A set partitioning model of a wedding seating problem
|
2123
|
+
# Authors: Stuart Mitchell 2009
|
2124
|
+
|
2125
|
+
max_tables = 5
|
2126
|
+
max_table_size = 4
|
2127
|
+
guests = %w(A B C D E F G I J K L M N O P Q R)
|
2128
|
+
|
2129
|
+
# Find the happiness of the table
|
2130
|
+
# by calculating the maximum distance between the letters
|
2131
|
+
def happiness(table)
|
2132
|
+
(table[0].ord - table[-1].ord).abs
|
2133
|
+
end
|
2134
|
+
|
2135
|
+
# create list of all possible tables
|
2136
|
+
possible_tables = []
|
2137
|
+
(1..max_table_size).each do |i|
|
2138
|
+
possible_tables += guests.combination(i).to_a
|
2139
|
+
end
|
2140
|
+
|
2141
|
+
solver = ORTools::Solver.new("Wedding Seating Model", :cbc)
|
2142
|
+
|
2143
|
+
# create a binary variable to state that a table setting is used
|
2144
|
+
x = {}
|
2145
|
+
possible_tables.each do |table|
|
2146
|
+
x[table] = solver.int_var(0, 1, "table #{table.join(", ")}")
|
2147
|
+
end
|
2148
|
+
|
2149
|
+
solver.minimize(solver.sum(possible_tables.map { |table| x[table] * happiness(table) }))
|
2150
|
+
|
2151
|
+
# specify the maximum number of tables
|
2152
|
+
solver.add(solver.sum(x.values) <= max_tables)
|
2153
|
+
|
2154
|
+
# a guest must seated at one and only one table
|
2155
|
+
guests.each do |guest|
|
2156
|
+
tables_with_guest = possible_tables.select { |table| table.include?(guest) }
|
2157
|
+
solver.add(solver.sum(tables_with_guest.map { |table| x[table] }) == 1)
|
2158
|
+
end
|
2159
|
+
|
2160
|
+
status = solver.solve
|
2161
|
+
|
2162
|
+
puts "The chosen tables are out of a total of %s:" % possible_tables.size
|
2163
|
+
possible_tables.each do |table|
|
2164
|
+
if x[table].solution_value == 1
|
2165
|
+
p table
|
2166
|
+
end
|
2167
|
+
end
|
2168
|
+
```
|
2169
|
+
|
1088
2170
|
## History
|
1089
2171
|
|
1090
2172
|
View the [changelog](https://github.com/ankane/or-tools/blob/master/CHANGELOG.md)
|
@@ -1104,7 +2186,7 @@ To get started with development:
|
|
1104
2186
|
git clone https://github.com/ankane/or-tools.git
|
1105
2187
|
cd or-tools
|
1106
2188
|
bundle install
|
1107
|
-
bundle exec rake compile
|
2189
|
+
bundle exec rake compile
|
1108
2190
|
bundle exec rake test
|
1109
2191
|
```
|
1110
2192
|
|