or-tools 0.1.2 → 0.3.0
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 +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
|
|