or-tools 0.1.3 → 0.3.1
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 +24 -0
- data/README.md +732 -439
- data/ext/or-tools/ext.cpp +114 -11
- data/ext/or-tools/extconf.rb +21 -63
- data/ext/or-tools/vendor.rb +95 -0
- data/lib/or-tools.rb +7 -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 +4 -0
- data/lib/or_tools/linear_expr.rb +8 -0
- data/lib/or_tools/seating.rb +115 -0
- data/lib/or_tools/sudoku.rb +132 -0
- data/lib/or_tools/tsp.rb +60 -0
- data/lib/or_tools/version.rb +1 -1
- metadata +8 -3
- data/lib/or_tools/ext.bundle +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 116cfa149eef398630f24b3776c9db8e0a1dddb2b17c8e4b04f347f064231c72
|
4
|
+
data.tar.gz: 9930c7e37bc26fd88a6c59eac718eb057d82990bc3ccb3bee3f395456baa9318
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eab3682edbfa9d3a90eb54ffe1697abd9f9e4dcb75ae6e08b08dfea1dcaedce4d773d3d53490d4c9de258d4d0e3b92e38bcb1cb4c5f4f50225e829c566b2c74b
|
7
|
+
data.tar.gz: '0514577229f62526c914a52b7164a2dc49b17515cbe798cd177f3a9dadc1da4a9026c45cec77830b08ac2bce44831f072ee18c6e3bdb9db66d958b6ed407d7b2'
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,27 @@
|
|
1
|
+
## 0.3.1 (2020-07-21)
|
2
|
+
|
3
|
+
- Reduced gem size
|
4
|
+
|
5
|
+
## 0.3.0 (2020-07-21)
|
6
|
+
|
7
|
+
- Updated OR-Tools to 7.7
|
8
|
+
- Added `BasicScheduler` class
|
9
|
+
- Added `Seating` class
|
10
|
+
- Added `TSP` class
|
11
|
+
- Added `Sudoku` class
|
12
|
+
|
13
|
+
## 0.2.0 (2020-05-22)
|
14
|
+
|
15
|
+
- No longer need to download the OR-Tools C++ library separately on Mac, Ubuntu 18.04, Ubuntu 16.04, Debian 10, and CentOS 8
|
16
|
+
|
17
|
+
## 0.1.5 (2020-04-23)
|
18
|
+
|
19
|
+
- Added support for OR-Tools 7.6
|
20
|
+
|
21
|
+
## 0.1.4 (2020-04-19)
|
22
|
+
|
23
|
+
- Added support for the Job Shop Problem
|
24
|
+
|
1
25
|
## 0.1.3 (2020-03-24)
|
2
26
|
|
3
27
|
- Added support for more routing problems
|
data/README.md
CHANGED
@@ -6,24 +6,261 @@
|
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
9
|
-
|
9
|
+
Add this line to your application’s Gemfile:
|
10
10
|
|
11
|
-
```
|
12
|
-
|
11
|
+
```ruby
|
12
|
+
gem 'or-tools'
|
13
13
|
```
|
14
14
|
|
15
|
-
|
15
|
+
Installation can take a few minutes as OR-Tools downloads and builds.
|
16
|
+
|
17
|
+
## Higher Level Interfaces
|
18
|
+
|
19
|
+
- [Scheduling](#scheduling)
|
20
|
+
- [Seating](#seating)
|
21
|
+
- [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
|
22
|
+
- [Sudoku](#sudoku)
|
23
|
+
|
24
|
+
### Scheduling
|
25
|
+
|
26
|
+
Specify people and their availabililty
|
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)
|
@@ -31,13 +268,14 @@ Constraint Optimization
|
|
31
268
|
- [Cryptarithmetic](#cryptarithmetic)
|
32
269
|
- [The N-queens Problem](#the-n-queens-problem)
|
33
270
|
|
34
|
-
|
271
|
+
Assignment
|
35
272
|
|
36
|
-
- [
|
273
|
+
- [Assignment](#assignment)
|
274
|
+
- [Assignment with Teams](#assignment-with-teams)
|
37
275
|
|
38
276
|
Routing
|
39
277
|
|
40
|
-
- [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
|
278
|
+
- [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp-1)
|
41
279
|
- [Vehicle Routing Problem (VRP)](#vehicle-routing-problem-vrp)
|
42
280
|
- [Capacity Constraints](#capacity-constraints)
|
43
281
|
- [Pickups and Deliveries](#pickups-and-deliveries)
|
@@ -56,42 +294,32 @@ Network Flows
|
|
56
294
|
|
57
295
|
- [Maximum Flows](#maximum-flows)
|
58
296
|
- [Minimum Cost Flows](#minimum-cost-flows)
|
59
|
-
|
60
|
-
Assignment
|
61
|
-
|
62
|
-
- [Assignment](#assignment)
|
63
|
-
- [Assignment as a Min Cost Problem](#assignment-as-a-min-cost-problem)
|
64
|
-
- [Assignment as a MIP Problem](#assignment-as-a-mip-problem)
|
297
|
+
- [Assignment as a Min Cost Flow Problem](#assignment-as-a-min-cost-flow-problem)
|
65
298
|
|
66
299
|
Scheduling
|
67
300
|
|
68
301
|
- [Employee Scheduling](#employee-scheduling)
|
302
|
+
- [The Job Shop Problem](#the-job-shop-problem)
|
69
303
|
|
70
304
|
Other Examples
|
71
305
|
|
72
|
-
- [Sudoku](#sudoku)
|
306
|
+
- [Sudoku](#sudoku-1)
|
307
|
+
- [Wedding Seating Chart](#wedding-seating-chart)
|
73
308
|
- [Set Partitioning](#set-partitioning)
|
74
309
|
|
75
310
|
### The Glop Linear Solver
|
76
311
|
|
77
312
|
[Guide](https://developers.google.com/optimization/lp/glop)
|
78
313
|
|
79
|
-
Declare the solver
|
80
|
-
|
81
314
|
```ruby
|
315
|
+
# declare the solver
|
82
316
|
solver = ORTools::Solver.new("LinearProgrammingExample", :glop)
|
83
|
-
```
|
84
|
-
|
85
|
-
Create the variables
|
86
317
|
|
87
|
-
|
318
|
+
# create the variables
|
88
319
|
x = solver.num_var(0, solver.infinity, "x")
|
89
320
|
y = solver.num_var(0, solver.infinity, "y")
|
90
|
-
```
|
91
|
-
|
92
|
-
Define the constraints
|
93
321
|
|
94
|
-
|
322
|
+
# define the constraints
|
95
323
|
constraint0 = solver.constraint(-solver.infinity, 14)
|
96
324
|
constraint0.set_coefficient(x, 1)
|
97
325
|
constraint0.set_coefficient(y, 2)
|
@@ -103,26 +331,17 @@ constraint1.set_coefficient(y, -1)
|
|
103
331
|
constraint2 = solver.constraint(-solver.infinity, 2)
|
104
332
|
constraint2.set_coefficient(x, 1)
|
105
333
|
constraint2.set_coefficient(y, -1)
|
106
|
-
```
|
107
|
-
|
108
|
-
Define the objective function
|
109
334
|
|
110
|
-
|
335
|
+
# define the objective function
|
111
336
|
objective = solver.objective
|
112
337
|
objective.set_coefficient(x, 3)
|
113
338
|
objective.set_coefficient(y, 4)
|
114
339
|
objective.set_maximization
|
115
|
-
```
|
116
|
-
|
117
|
-
Invoke the solver
|
118
340
|
|
119
|
-
|
341
|
+
# invoke the solver
|
120
342
|
solver.solve
|
121
|
-
```
|
122
|
-
|
123
|
-
Display the solution
|
124
343
|
|
125
|
-
|
344
|
+
# display the solution
|
126
345
|
opt_solution = 3 * x.solution_value + 4 * y.solution_value
|
127
346
|
puts "Number of variables = #{solver.num_variables}"
|
128
347
|
puts "Number of constraints = #{solver.num_constraints}"
|
@@ -132,42 +351,75 @@ puts "y = #{y.solution_value}"
|
|
132
351
|
puts "Optimal objective value = #{opt_solution}"
|
133
352
|
```
|
134
353
|
|
135
|
-
###
|
136
|
-
|
137
|
-
[Guide](https://developers.google.com/optimization/cp/cp_solver)
|
354
|
+
### Mixed-Integer Programming
|
138
355
|
|
139
|
-
|
356
|
+
[Guide](https://developers.google.com/optimization/mip/integer_opt)
|
140
357
|
|
141
358
|
```ruby
|
142
|
-
|
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
|
143
398
|
```
|
144
399
|
|
145
|
-
|
400
|
+
### CP-SAT Solver
|
401
|
+
|
402
|
+
[Guide](https://developers.google.com/optimization/cp/cp_solver)
|
146
403
|
|
147
404
|
```ruby
|
405
|
+
# declare the model
|
406
|
+
model = ORTools::CpModel.new
|
407
|
+
|
408
|
+
# create the variables
|
148
409
|
num_vals = 3
|
149
410
|
x = model.new_int_var(0, num_vals - 1, "x")
|
150
411
|
y = model.new_int_var(0, num_vals - 1, "y")
|
151
412
|
z = model.new_int_var(0, num_vals - 1, "z")
|
152
|
-
```
|
153
413
|
|
154
|
-
|
155
|
-
|
156
|
-
```ruby
|
414
|
+
# create the constraint
|
157
415
|
model.add(x != y)
|
158
|
-
```
|
159
416
|
|
160
|
-
|
161
|
-
|
162
|
-
```ruby
|
417
|
+
# call the solver
|
163
418
|
solver = ORTools::CpSolver.new
|
164
419
|
status = solver.solve(model)
|
165
|
-
```
|
166
420
|
|
167
|
-
|
168
|
-
|
169
|
-
```ruby
|
170
|
-
if status == :feasible
|
421
|
+
# display the first solution
|
422
|
+
if status == :optimal
|
171
423
|
puts "x = #{solver.value(x)}"
|
172
424
|
puts "y = #{solver.value(y)}"
|
173
425
|
puts "z = #{solver.value(z)}"
|
@@ -178,38 +430,25 @@ end
|
|
178
430
|
|
179
431
|
[Guide](https://developers.google.com/optimization/cp/integer_opt_cp)
|
180
432
|
|
181
|
-
Declare the model
|
182
|
-
|
183
433
|
```ruby
|
434
|
+
# declare the model
|
184
435
|
model = ORTools::CpModel.new
|
185
|
-
```
|
186
|
-
|
187
|
-
Create the variables
|
188
436
|
|
189
|
-
|
437
|
+
# create the variables
|
190
438
|
var_upper_bound = [50, 45, 37].max
|
191
439
|
x = model.new_int_var(0, var_upper_bound, "x")
|
192
440
|
y = model.new_int_var(0, var_upper_bound, "y")
|
193
441
|
z = model.new_int_var(0, var_upper_bound, "z")
|
194
|
-
```
|
195
|
-
|
196
|
-
Define the constraints
|
197
442
|
|
198
|
-
|
443
|
+
# define the constraints
|
199
444
|
model.add(x*2 + y*7 + z*3 <= 50)
|
200
445
|
model.add(x*3 - y*5 + z*7 <= 45)
|
201
446
|
model.add(x*5 + y*2 - z*6 <= 37)
|
202
|
-
```
|
203
|
-
|
204
|
-
Define the objective function
|
205
447
|
|
206
|
-
|
448
|
+
# define the objective function
|
207
449
|
model.maximize(x*2 + y*2 + z*3)
|
208
|
-
```
|
209
|
-
|
210
|
-
Call the solver
|
211
450
|
|
212
|
-
|
451
|
+
# call the solver
|
213
452
|
solver = ORTools::CpSolver.new
|
214
453
|
status = solver.solve(model)
|
215
454
|
|
@@ -226,9 +465,8 @@ end
|
|
226
465
|
|
227
466
|
[Guide](https://developers.google.com/optimization/cp/cryptarithmetic)
|
228
467
|
|
229
|
-
Define the variables
|
230
|
-
|
231
468
|
```ruby
|
469
|
+
# define the variables
|
232
470
|
model = ORTools::CpModel.new
|
233
471
|
|
234
472
|
base = 10
|
@@ -245,20 +483,14 @@ r = model.new_int_var(0, base - 1, "R")
|
|
245
483
|
e = model.new_int_var(0, base - 1, "E")
|
246
484
|
|
247
485
|
letters = [c, p, i, s, f, u, n, t, r, e]
|
248
|
-
```
|
249
486
|
|
250
|
-
|
251
|
-
|
252
|
-
```ruby
|
487
|
+
# define the constraints
|
253
488
|
model.add_all_different(letters)
|
254
489
|
|
255
490
|
model.add(c * base + p + i * base + s + f * base * base + u * base +
|
256
491
|
n == t * base * base * base + r * base * base + u * base + e)
|
257
|
-
```
|
258
492
|
|
259
|
-
|
260
|
-
|
261
|
-
```ruby
|
493
|
+
# define the solution printer
|
262
494
|
class VarArraySolutionPrinter < ORTools::CpSolverSolutionCallback
|
263
495
|
attr_reader :solution_count
|
264
496
|
|
@@ -276,11 +508,8 @@ class VarArraySolutionPrinter < ORTools::CpSolverSolutionCallback
|
|
276
508
|
puts
|
277
509
|
end
|
278
510
|
end
|
279
|
-
```
|
280
511
|
|
281
|
-
|
282
|
-
|
283
|
-
```ruby
|
512
|
+
# invoke the solver
|
284
513
|
solver = ORTools::CpSolver.new
|
285
514
|
solution_printer = VarArraySolutionPrinter.new(letters)
|
286
515
|
status = solver.search_for_all_solutions(model, solution_printer)
|
@@ -298,22 +527,15 @@ puts " - solutions found : %i" % solution_printer.solution_count
|
|
298
527
|
|
299
528
|
[Guide](https://developers.google.com/optimization/cp/queens)
|
300
529
|
|
301
|
-
Declare the model
|
302
|
-
|
303
530
|
```ruby
|
531
|
+
# declare the model
|
304
532
|
board_size = 8
|
305
533
|
model = ORTools::CpModel.new
|
306
|
-
```
|
307
|
-
|
308
|
-
Create the variables
|
309
534
|
|
310
|
-
|
535
|
+
# create the variables
|
311
536
|
queens = board_size.times.map { |i| model.new_int_var(0, board_size - 1, "x%i" % i) }
|
312
|
-
```
|
313
|
-
|
314
|
-
Create the constraints
|
315
537
|
|
316
|
-
|
538
|
+
# create the constraints
|
317
539
|
board_size.times do |i|
|
318
540
|
diag1 = []
|
319
541
|
diag2 = []
|
@@ -328,11 +550,8 @@ board_size.times do |i|
|
|
328
550
|
model.add_all_different(diag1)
|
329
551
|
model.add_all_different(diag2)
|
330
552
|
end
|
331
|
-
```
|
332
|
-
|
333
|
-
Create a solution printer
|
334
553
|
|
335
|
-
|
554
|
+
# create a solution printer
|
336
555
|
class SolutionPrinter < ORTools::CpSolverSolutionCallback
|
337
556
|
attr_reader :solution_count
|
338
557
|
|
@@ -350,11 +569,8 @@ class SolutionPrinter < ORTools::CpSolverSolutionCallback
|
|
350
569
|
puts
|
351
570
|
end
|
352
571
|
end
|
353
|
-
```
|
354
|
-
|
355
|
-
Call the solver and display the results
|
356
572
|
|
357
|
-
|
573
|
+
# call the solver and display the results
|
358
574
|
solver = ORTools::CpSolver.new
|
359
575
|
solution_printer = SolutionPrinter.new(queens)
|
360
576
|
status = solver.search_for_all_solutions(model, solution_printer)
|
@@ -362,75 +578,127 @@ puts
|
|
362
578
|
puts "Solutions found : %i" % solution_printer.solution_count
|
363
579
|
```
|
364
580
|
|
365
|
-
### Mixed-Integer Programming
|
366
581
|
|
367
|
-
|
582
|
+
### Assignment
|
368
583
|
|
369
|
-
|
584
|
+
[Guide](https://developers.google.com/optimization/assignment/assignment_example)
|
370
585
|
|
371
586
|
```ruby
|
372
|
-
|
373
|
-
|
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
|
374
595
|
|
375
|
-
|
596
|
+
# create the solver
|
597
|
+
assignment = ORTools::LinearSumAssignment.new
|
376
598
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
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
|
381
607
|
|
382
|
-
|
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
|
383
625
|
```
|
384
626
|
|
385
|
-
|
627
|
+
### Assignment with Teams
|
628
|
+
|
629
|
+
[Guide](https://developers.google.com/optimization/assignment/assignment_teams)
|
386
630
|
|
387
631
|
```ruby
|
388
|
-
|
389
|
-
|
390
|
-
c0.set_coefficient(y, 7)
|
632
|
+
# create the solver
|
633
|
+
solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
|
391
634
|
|
392
|
-
|
393
|
-
|
394
|
-
|
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]]
|
395
642
|
|
396
|
-
|
397
|
-
|
643
|
+
team1 = [0, 2, 4]
|
644
|
+
team2 = [1, 3, 5]
|
645
|
+
team_max = 2
|
398
646
|
|
399
|
-
|
647
|
+
# create the variables
|
648
|
+
num_workers = cost.length
|
649
|
+
num_tasks = cost[1].length
|
650
|
+
x = {}
|
400
651
|
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
```
|
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
|
407
657
|
|
408
|
-
|
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
|
+
))
|
409
662
|
|
410
|
-
|
411
|
-
|
412
|
-
|
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
|
413
667
|
|
414
|
-
|
668
|
+
num_tasks.times do |j|
|
669
|
+
solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
|
670
|
+
end
|
415
671
|
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
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
|
424
690
|
end
|
691
|
+
|
692
|
+
puts
|
693
|
+
puts "Time = #{solver.wall_time} milliseconds"
|
425
694
|
```
|
426
695
|
|
427
696
|
### Traveling Salesperson Problem (TSP)
|
428
697
|
|
429
698
|
[Guide](https://developers.google.com/optimization/routing/tsp.html)
|
430
699
|
|
431
|
-
Create the data
|
432
|
-
|
433
700
|
```ruby
|
701
|
+
# create the data
|
434
702
|
data = {}
|
435
703
|
data[:distance_matrix] = [
|
436
704
|
[0, 2451, 713, 1018, 1631, 1374, 2408, 213, 2571, 875, 1420, 2145, 1972],
|
@@ -449,11 +717,8 @@ data[:distance_matrix] = [
|
|
449
717
|
]
|
450
718
|
data[:num_vehicles] = 1
|
451
719
|
data[:depot] = 0
|
452
|
-
```
|
453
|
-
|
454
|
-
Create the distance callback
|
455
720
|
|
456
|
-
|
721
|
+
# create the distance callback
|
457
722
|
manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].length, data[:num_vehicles], data[:depot])
|
458
723
|
routing = ORTools::RoutingModel.new(manager)
|
459
724
|
|
@@ -465,17 +730,11 @@ end
|
|
465
730
|
|
466
731
|
transit_callback_index = routing.register_transit_callback(distance_callback)
|
467
732
|
routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
|
468
|
-
```
|
469
|
-
|
470
|
-
Run the solver
|
471
|
-
|
472
|
-
```ruby
|
473
|
-
assignment = routing.solve(first_solution_strategy: :path_cheaper_arc)
|
474
|
-
```
|
475
733
|
|
476
|
-
|
734
|
+
# run the solver
|
735
|
+
assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
|
477
736
|
|
478
|
-
|
737
|
+
# print the solution
|
479
738
|
puts "Objective: #{assignment.objective_value} miles"
|
480
739
|
index = routing.start(0)
|
481
740
|
plan_output = String.new("Route for vehicle 0:\n")
|
@@ -494,9 +753,8 @@ puts plan_output
|
|
494
753
|
|
495
754
|
[Guide](https://developers.google.com/optimization/routing/vrp)
|
496
755
|
|
497
|
-
Create the data
|
498
|
-
|
499
756
|
```ruby
|
757
|
+
# create the data
|
500
758
|
data = {}
|
501
759
|
data[:distance_matrix] = [
|
502
760
|
[0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],
|
@@ -519,11 +777,8 @@ data[:distance_matrix] = [
|
|
519
777
|
]
|
520
778
|
data[:num_vehicles] = 4
|
521
779
|
data[:depot] = 0
|
522
|
-
```
|
523
780
|
|
524
|
-
|
525
|
-
|
526
|
-
```ruby
|
781
|
+
# define the distance callback
|
527
782
|
manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].length, data[:num_vehicles], data[:depot])
|
528
783
|
routing = ORTools::RoutingModel.new(manager)
|
529
784
|
|
@@ -535,26 +790,17 @@ end
|
|
535
790
|
|
536
791
|
transit_callback_index = routing.register_transit_callback(distance_callback)
|
537
792
|
routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
|
538
|
-
```
|
539
793
|
|
540
|
-
|
541
|
-
|
542
|
-
```ruby
|
794
|
+
# add a distance dimension
|
543
795
|
dimension_name = "Distance"
|
544
796
|
routing.add_dimension(transit_callback_index, 0, 3000, true, dimension_name)
|
545
797
|
distance_dimension = routing.mutable_dimension(dimension_name)
|
546
798
|
distance_dimension.global_span_cost_coefficient = 100
|
547
|
-
```
|
548
799
|
|
549
|
-
|
550
|
-
|
551
|
-
```ruby
|
800
|
+
# run the solver
|
552
801
|
solution = routing.solve(first_solution_strategy: :path_cheapest_arc)
|
553
|
-
```
|
554
802
|
|
555
|
-
|
556
|
-
|
557
|
-
```ruby
|
803
|
+
# print the solution
|
558
804
|
max_route_distance = 0
|
559
805
|
data[:num_vehicles].times do |vehicle_id|
|
560
806
|
index = routing.start(vehicle_id)
|
@@ -698,7 +944,6 @@ data[:num_vehicles] = 4
|
|
698
944
|
data[:depot] = 0
|
699
945
|
|
700
946
|
manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].size, data[:num_vehicles], data[:depot])
|
701
|
-
|
702
947
|
routing = ORTools::RoutingModel.new(manager)
|
703
948
|
|
704
949
|
distance_callback = lambda do |from_index, to_index|
|
@@ -906,7 +1151,6 @@ data[:depot_capacity] = 2
|
|
906
1151
|
data[:depot] = 0
|
907
1152
|
|
908
1153
|
manager = ORTools::RoutingIndexManager.new(data[:time_matrix].size, data[:num_vehicles], data[:depot])
|
909
|
-
|
910
1154
|
routing = ORTools::RoutingModel.new(manager)
|
911
1155
|
|
912
1156
|
time_callback = lambda do |from_index, to_index|
|
@@ -1014,7 +1258,6 @@ data[:num_vehicles] = 4
|
|
1014
1258
|
data[:depot] = 0
|
1015
1259
|
|
1016
1260
|
manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].size, data[:num_vehicles], data[:depot])
|
1017
|
-
|
1018
1261
|
routing = ORTools::RoutingModel.new(manager)
|
1019
1262
|
|
1020
1263
|
distance_callback = lambda do |from_index, to_index|
|
@@ -1103,9 +1346,8 @@ routing.solve(
|
|
1103
1346
|
|
1104
1347
|
[Guide](https://developers.google.com/optimization/bin/knapsack)
|
1105
1348
|
|
1106
|
-
Create the data
|
1107
|
-
|
1108
1349
|
```ruby
|
1350
|
+
# create the data
|
1109
1351
|
values = [
|
1110
1352
|
360, 83, 59, 130, 431, 67, 230, 52, 93, 125, 670, 892, 600, 38, 48, 147,
|
1111
1353
|
78, 256, 63, 17, 120, 164, 432, 35, 92, 110, 22, 42, 50, 323, 514, 28,
|
@@ -1118,17 +1360,11 @@ weights = [[
|
|
1118
1360
|
3, 86, 66, 31, 65, 0, 79, 20, 65, 52, 13
|
1119
1361
|
]]
|
1120
1362
|
capacities = [850]
|
1121
|
-
```
|
1122
|
-
|
1123
|
-
Declare the solver
|
1124
1363
|
|
1125
|
-
|
1364
|
+
# declare the solver
|
1126
1365
|
solver = ORTools::KnapsackSolver.new(:branch_and_bound, "KnapsackExample")
|
1127
|
-
```
|
1128
|
-
|
1129
|
-
Call the solver
|
1130
1366
|
|
1131
|
-
|
1367
|
+
# call the solver
|
1132
1368
|
solver.init(values, weights, capacities)
|
1133
1369
|
computed_value = solver.solve
|
1134
1370
|
|
@@ -1152,9 +1388,8 @@ puts "Packed weights: #{packed_weights}"
|
|
1152
1388
|
|
1153
1389
|
[Guide](https://developers.google.com/optimization/bin/multiple_knapsack)
|
1154
1390
|
|
1155
|
-
Create the data
|
1156
|
-
|
1157
1391
|
```ruby
|
1392
|
+
# create the data
|
1158
1393
|
data = {}
|
1159
1394
|
weights = [48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36]
|
1160
1395
|
values = [10, 30, 25, 50, 35, 30, 15, 40, 30, 35, 45, 10, 20, 30, 25]
|
@@ -1165,28 +1400,21 @@ data[:num_items] = weights.length
|
|
1165
1400
|
num_bins = 5
|
1166
1401
|
data[:bins] = (0...num_bins).to_a
|
1167
1402
|
data[:bin_capacities] = [100, 100, 100, 100, 100]
|
1168
|
-
```
|
1169
|
-
|
1170
|
-
Declare the solver
|
1171
1403
|
|
1172
|
-
|
1404
|
+
# declare the solver
|
1173
1405
|
solver = ORTools::Solver.new("simple_mip_program", :cbc)
|
1174
|
-
```
|
1175
|
-
|
1176
|
-
Create the variables
|
1177
1406
|
|
1178
|
-
|
1407
|
+
# create the variables
|
1408
|
+
# x[i, j] = 1 if item i is packed in bin j
|
1179
1409
|
x = {}
|
1180
1410
|
data[:items].each do |i|
|
1181
1411
|
data[:bins].each do |j|
|
1182
1412
|
x[[i, j]] = solver.int_var(0, 1, "x_%i_%i" % [i, j])
|
1183
1413
|
end
|
1184
1414
|
end
|
1185
|
-
```
|
1186
1415
|
|
1187
|
-
|
1188
|
-
|
1189
|
-
```ruby
|
1416
|
+
# define the constraints
|
1417
|
+
# each item can be in at most one bin
|
1190
1418
|
data[:items].each do |i|
|
1191
1419
|
sum = ORTools::LinearExpr.new
|
1192
1420
|
data[:bins].each do |j|
|
@@ -1195,6 +1423,7 @@ data[:items].each do |i|
|
|
1195
1423
|
solver.add(sum <= 1.0)
|
1196
1424
|
end
|
1197
1425
|
|
1426
|
+
# the amount packed in each bin cannot exceed its capacity
|
1198
1427
|
data[:bins].each do |j|
|
1199
1428
|
weight = ORTools::LinearExpr.new
|
1200
1429
|
data[:items].each do |i|
|
@@ -1202,11 +1431,8 @@ data[:bins].each do |j|
|
|
1202
1431
|
end
|
1203
1432
|
solver.add(weight <= data[:bin_capacities][j])
|
1204
1433
|
end
|
1205
|
-
```
|
1206
|
-
|
1207
|
-
Define the objective
|
1208
1434
|
|
1209
|
-
|
1435
|
+
# define the objective
|
1210
1436
|
objective = solver.objective
|
1211
1437
|
|
1212
1438
|
data[:items].each do |i|
|
@@ -1215,11 +1441,8 @@ data[:items].each do |i|
|
|
1215
1441
|
end
|
1216
1442
|
end
|
1217
1443
|
objective.set_maximization
|
1218
|
-
```
|
1219
1444
|
|
1220
|
-
|
1221
|
-
|
1222
|
-
```ruby
|
1445
|
+
# call the solver and print the solution
|
1223
1446
|
status = solver.solve
|
1224
1447
|
|
1225
1448
|
if status == :optimal
|
@@ -1251,26 +1474,20 @@ end
|
|
1251
1474
|
|
1252
1475
|
[Guide](https://developers.google.com/optimization/bin/bin_packing)
|
1253
1476
|
|
1254
|
-
Create the data
|
1255
|
-
|
1256
1477
|
```ruby
|
1478
|
+
# create the data
|
1257
1479
|
data = {}
|
1258
1480
|
weights = [48, 30, 19, 36, 36, 27, 42, 42, 36, 24, 30]
|
1259
1481
|
data[:weights] = weights
|
1260
1482
|
data[:items] = (0...weights.length).to_a
|
1261
1483
|
data[:bins] = data[:items]
|
1262
1484
|
data[:bin_capacity] = 100
|
1263
|
-
```
|
1264
1485
|
|
1265
|
-
|
1266
|
-
|
1267
|
-
```ruby
|
1486
|
+
# create the mip solver with the CBC backend
|
1268
1487
|
solver = ORTools::Solver.new("simple_mip_program", :cbc)
|
1269
|
-
```
|
1270
|
-
|
1271
|
-
Create the variables
|
1272
1488
|
|
1273
|
-
|
1489
|
+
# variables
|
1490
|
+
# x[i, j] = 1 if item i is packed in bin j
|
1274
1491
|
x = {}
|
1275
1492
|
data[:items].each do |i|
|
1276
1493
|
data[:bins].each do |j|
|
@@ -1278,34 +1495,28 @@ data[:items].each do |i|
|
|
1278
1495
|
end
|
1279
1496
|
end
|
1280
1497
|
|
1498
|
+
# y[j] = 1 if bin j is used
|
1281
1499
|
y = {}
|
1282
1500
|
data[:bins].each do |j|
|
1283
1501
|
y[j] = solver.int_var(0, 1, "y[%i]" % j)
|
1284
1502
|
end
|
1285
|
-
```
|
1286
1503
|
|
1287
|
-
|
1288
|
-
|
1289
|
-
```ruby
|
1504
|
+
# constraints
|
1505
|
+
# each item must be in exactly one bin
|
1290
1506
|
data[:items].each do |i|
|
1291
1507
|
solver.add(solver.sum(data[:bins].map { |j| x[[i, j]] }) == 1)
|
1292
1508
|
end
|
1293
1509
|
|
1510
|
+
# the amount packed in each bin cannot exceed its capacity
|
1294
1511
|
data[:bins].each do |j|
|
1295
1512
|
sum = solver.sum(data[:items].map { |i| x[[i, j]] * data[:weights][i] })
|
1296
1513
|
solver.add(sum <= y[j] * data[:bin_capacity])
|
1297
1514
|
end
|
1298
|
-
```
|
1299
|
-
|
1300
|
-
Define the objective
|
1301
1515
|
|
1302
|
-
|
1516
|
+
# objective: minimize the number of bins used
|
1303
1517
|
solver.minimize(solver.sum(data[:bins].map { |j| y[j] }))
|
1304
|
-
```
|
1305
1518
|
|
1306
|
-
|
1307
|
-
|
1308
|
-
```ruby
|
1519
|
+
# call the solver and print the solution
|
1309
1520
|
if status == :optimal
|
1310
1521
|
num_bins = 0
|
1311
1522
|
data[:bins].each do |j|
|
@@ -1339,27 +1550,20 @@ end
|
|
1339
1550
|
|
1340
1551
|
[Guide](https://developers.google.com/optimization/flow/maxflow)
|
1341
1552
|
|
1342
|
-
Define the data
|
1343
|
-
|
1344
1553
|
```ruby
|
1554
|
+
# define the data
|
1345
1555
|
start_nodes = [0, 0, 0, 1, 1, 2, 2, 3, 3]
|
1346
1556
|
end_nodes = [1, 2, 3, 2, 4, 3, 4, 2, 4]
|
1347
1557
|
capacities = [20, 30, 10, 40, 30, 10, 20, 5, 20]
|
1348
|
-
```
|
1349
1558
|
|
1350
|
-
|
1351
|
-
|
1352
|
-
```ruby
|
1559
|
+
# declare the solver and add the arcs
|
1353
1560
|
max_flow = ORTools::SimpleMaxFlow.new
|
1354
1561
|
|
1355
1562
|
start_nodes.length.times do |i|
|
1356
1563
|
max_flow.add_arc_with_capacity(start_nodes[i], end_nodes[i], capacities[i])
|
1357
1564
|
end
|
1358
|
-
```
|
1359
|
-
|
1360
|
-
Invoke the solver and display the results
|
1361
1565
|
|
1362
|
-
|
1566
|
+
# invoke the solver and display the results
|
1363
1567
|
if max_flow.solve(0, 4) == :optimal
|
1364
1568
|
puts "Max flow: #{max_flow.optimal_flow}"
|
1365
1569
|
puts
|
@@ -1383,19 +1587,15 @@ end
|
|
1383
1587
|
|
1384
1588
|
[Guide](https://developers.google.com/optimization/flow/mincostflow)
|
1385
1589
|
|
1386
|
-
Define the data
|
1387
|
-
|
1388
1590
|
```ruby
|
1591
|
+
# define the data
|
1389
1592
|
start_nodes = [ 0, 0, 1, 1, 1, 2, 2, 3, 4]
|
1390
1593
|
end_nodes = [ 1, 2, 2, 3, 4, 3, 4, 4, 2]
|
1391
1594
|
capacities = [15, 8, 20, 4, 10, 15, 4, 20, 5]
|
1392
1595
|
unit_costs = [ 4, 4, 2, 2, 6, 1, 3, 2, 3]
|
1393
1596
|
supplies = [20, 0, 0, -5, -15]
|
1394
|
-
```
|
1395
|
-
|
1396
|
-
Declare the solver and add the arcs
|
1397
1597
|
|
1398
|
-
|
1598
|
+
# declare the solver and add the arcs
|
1399
1599
|
min_cost_flow = ORTools::SimpleMinCostFlow.new
|
1400
1600
|
|
1401
1601
|
start_nodes.length.times do |i|
|
@@ -1407,11 +1607,8 @@ end
|
|
1407
1607
|
supplies.length.times do |i|
|
1408
1608
|
min_cost_flow.set_node_supply(i, supplies[i])
|
1409
1609
|
end
|
1410
|
-
```
|
1411
1610
|
|
1412
|
-
|
1413
|
-
|
1414
|
-
```ruby
|
1611
|
+
# invoke the solver and display the results
|
1415
1612
|
if min_cost_flow.solve == :optimal
|
1416
1613
|
puts "Minimum cost #{min_cost_flow.optimal_cost}"
|
1417
1614
|
puts
|
@@ -1431,74 +1628,15 @@ else
|
|
1431
1628
|
end
|
1432
1629
|
```
|
1433
1630
|
|
1434
|
-
|
1435
|
-
|
1436
|
-
[Guide](https://developers.google.com/optimization/assignment/simple_assignment)
|
1437
|
-
|
1438
|
-
Create the data
|
1439
|
-
|
1440
|
-
```ruby
|
1441
|
-
cost = [[ 90, 76, 75, 70],
|
1442
|
-
[ 35, 85, 55, 65],
|
1443
|
-
[125, 95, 90, 105],
|
1444
|
-
[ 45, 110, 95, 115]]
|
1445
|
-
|
1446
|
-
rows = cost.length
|
1447
|
-
cols = cost[0].length
|
1448
|
-
```
|
1449
|
-
|
1450
|
-
Create the solver
|
1451
|
-
|
1452
|
-
```ruby
|
1453
|
-
assignment = ORTools::LinearSumAssignment.new
|
1454
|
-
```
|
1455
|
-
|
1456
|
-
Add the costs to the solver
|
1457
|
-
|
1458
|
-
```ruby
|
1459
|
-
rows.times do |worker|
|
1460
|
-
cols.times do |task|
|
1461
|
-
if cost[worker][task]
|
1462
|
-
assignment.add_arc_with_cost(worker, task, cost[worker][task])
|
1463
|
-
end
|
1464
|
-
end
|
1465
|
-
end
|
1466
|
-
```
|
1467
|
-
|
1468
|
-
Invoke the solver
|
1469
|
-
|
1470
|
-
```ruby
|
1471
|
-
solve_status = assignment.solve
|
1472
|
-
if solve_status == :optimal
|
1473
|
-
puts "Total cost = #{assignment.optimal_cost}"
|
1474
|
-
puts
|
1475
|
-
assignment.num_nodes.times do |i|
|
1476
|
-
puts "Worker %d assigned to task %d. Cost = %d" % [
|
1477
|
-
i,
|
1478
|
-
assignment.right_mate(i),
|
1479
|
-
assignment.assignment_cost(i)
|
1480
|
-
]
|
1481
|
-
end
|
1482
|
-
elsif solve_status == :infeasible
|
1483
|
-
puts "No assignment is possible."
|
1484
|
-
elsif solve_status == :possible_overflow
|
1485
|
-
puts "Some input costs are too large and may cause an integer overflow."
|
1486
|
-
end
|
1487
|
-
```
|
1488
|
-
|
1489
|
-
## Assignment as a Min Cost Problem
|
1631
|
+
### Assignment as a Min Cost Flow Problem
|
1490
1632
|
|
1491
1633
|
[Guide](https://developers.google.com/optimization/assignment/assignment_min_cost_flow)
|
1492
1634
|
|
1493
|
-
Create the solver
|
1494
|
-
|
1495
1635
|
```ruby
|
1636
|
+
# create the solver
|
1496
1637
|
min_cost_flow = ORTools::SimpleMinCostFlow.new
|
1497
|
-
```
|
1498
|
-
|
1499
|
-
Create the data
|
1500
1638
|
|
1501
|
-
|
1639
|
+
# create the data
|
1502
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]
|
1503
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]
|
1504
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]
|
@@ -1507,11 +1645,8 @@ supplies = [4, 0, 0, 0, 0, 0, 0, 0, 0, -4]
|
|
1507
1645
|
source = 0
|
1508
1646
|
sink = 9
|
1509
1647
|
tasks = 4
|
1510
|
-
```
|
1511
1648
|
|
1512
|
-
|
1513
|
-
|
1514
|
-
```ruby
|
1649
|
+
# create the graph and constraints
|
1515
1650
|
start_nodes.length.times do |i|
|
1516
1651
|
min_cost_flow.add_arc_with_capacity_and_unit_cost(
|
1517
1652
|
start_nodes[i], end_nodes[i], capacities[i], costs[i]
|
@@ -1521,11 +1656,8 @@ end
|
|
1521
1656
|
supplies.length.times do |i|
|
1522
1657
|
min_cost_flow.set_node_supply(i, supplies[i])
|
1523
1658
|
end
|
1524
|
-
```
|
1525
|
-
|
1526
|
-
Invoke the solver
|
1527
1659
|
|
1528
|
-
|
1660
|
+
# invoke the solver
|
1529
1661
|
if min_cost_flow.solve == :optimal
|
1530
1662
|
puts "Total cost = #{min_cost_flow.optimal_cost}"
|
1531
1663
|
puts
|
@@ -1545,109 +1677,20 @@ else
|
|
1545
1677
|
end
|
1546
1678
|
```
|
1547
1679
|
|
1548
|
-
|
1549
|
-
|
1550
|
-
[Guide](https://developers.google.com/optimization/assignment/assignment_mip)
|
1551
|
-
|
1552
|
-
Create the solver
|
1553
|
-
|
1554
|
-
```ruby
|
1555
|
-
solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
|
1556
|
-
```
|
1557
|
-
|
1558
|
-
Create the data
|
1559
|
-
|
1560
|
-
```ruby
|
1561
|
-
cost = [[90, 76, 75, 70],
|
1562
|
-
[35, 85, 55, 65],
|
1563
|
-
[125, 95, 90, 105],
|
1564
|
-
[45, 110, 95, 115],
|
1565
|
-
[60, 105, 80, 75],
|
1566
|
-
[45, 65, 110, 95]]
|
1567
|
-
|
1568
|
-
team1 = [0, 2, 4]
|
1569
|
-
team2 = [1, 3, 5]
|
1570
|
-
team_max = 2
|
1571
|
-
```
|
1572
|
-
|
1573
|
-
Create the variables
|
1574
|
-
|
1575
|
-
```ruby
|
1576
|
-
num_workers = cost.length
|
1577
|
-
num_tasks = cost[1].length
|
1578
|
-
x = {}
|
1579
|
-
|
1580
|
-
num_workers.times do |i|
|
1581
|
-
num_tasks.times do |j|
|
1582
|
-
x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
|
1583
|
-
end
|
1584
|
-
end
|
1585
|
-
```
|
1586
|
-
|
1587
|
-
Create the objective function
|
1588
|
-
|
1589
|
-
```ruby
|
1590
|
-
solver.minimize(solver.sum(
|
1591
|
-
num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
|
1592
|
-
))
|
1593
|
-
```
|
1594
|
-
|
1595
|
-
Create the constraints
|
1596
|
-
|
1597
|
-
```ruby
|
1598
|
-
num_workers.times do |i|
|
1599
|
-
solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
|
1600
|
-
end
|
1601
|
-
|
1602
|
-
num_tasks.times do |j|
|
1603
|
-
solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
|
1604
|
-
end
|
1605
|
-
|
1606
|
-
solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
|
1607
|
-
solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
|
1608
|
-
```
|
1609
|
-
|
1610
|
-
Invoke the solver
|
1611
|
-
|
1612
|
-
```ruby
|
1613
|
-
sol = solver.solve
|
1614
|
-
|
1615
|
-
puts "Total cost = #{solver.objective.value}"
|
1616
|
-
puts
|
1617
|
-
num_workers.times do |i|
|
1618
|
-
num_tasks.times do |j|
|
1619
|
-
if x[[i, j]].solution_value > 0
|
1620
|
-
puts "Worker %d assigned to task %d. Cost = %d" % [
|
1621
|
-
i,
|
1622
|
-
j,
|
1623
|
-
cost[i][j]
|
1624
|
-
]
|
1625
|
-
end
|
1626
|
-
end
|
1627
|
-
end
|
1628
|
-
|
1629
|
-
puts
|
1630
|
-
puts "Time = #{solver.wall_time} milliseconds"
|
1631
|
-
```
|
1632
|
-
|
1633
|
-
## Employee Scheduling
|
1680
|
+
### Employee Scheduling
|
1634
1681
|
|
1635
1682
|
[Guide](https://developers.google.com/optimization/scheduling/employee_scheduling)
|
1636
1683
|
|
1637
|
-
Define the data
|
1638
|
-
|
1639
1684
|
```ruby
|
1685
|
+
# define the data
|
1640
1686
|
num_nurses = 4
|
1641
1687
|
num_shifts = 3
|
1642
1688
|
num_days = 3
|
1643
1689
|
all_nurses = num_nurses.times.to_a
|
1644
1690
|
all_shifts = num_shifts.times.to_a
|
1645
1691
|
all_days = num_days.times.to_a
|
1646
|
-
```
|
1647
1692
|
|
1648
|
-
|
1649
|
-
|
1650
|
-
```ruby
|
1693
|
+
# create the variables
|
1651
1694
|
model = ORTools::CpModel.new
|
1652
1695
|
|
1653
1696
|
shifts = {}
|
@@ -1658,11 +1701,8 @@ all_nurses.each do |n|
|
|
1658
1701
|
end
|
1659
1702
|
end
|
1660
1703
|
end
|
1661
|
-
```
|
1662
|
-
|
1663
|
-
Assign nurses to shifts
|
1664
1704
|
|
1665
|
-
|
1705
|
+
# assign nurses to shifts
|
1666
1706
|
all_days.each do |d|
|
1667
1707
|
all_shifts.each do |s|
|
1668
1708
|
model.add(model.sum(all_nurses.map { |n| shifts[[n, d, s]] }) == 1)
|
@@ -1674,11 +1714,8 @@ all_nurses.each do |n|
|
|
1674
1714
|
model.add(model.sum(all_shifts.map { |s| shifts[[n, d, s]] }) <= 1)
|
1675
1715
|
end
|
1676
1716
|
end
|
1677
|
-
```
|
1678
1717
|
|
1679
|
-
|
1680
|
-
|
1681
|
-
```ruby
|
1718
|
+
# assign shifts evenly
|
1682
1719
|
min_shifts_per_nurse = (num_shifts * num_days) / num_nurses
|
1683
1720
|
max_shifts_per_nurse = min_shifts_per_nurse + 1
|
1684
1721
|
all_nurses.each do |n|
|
@@ -1686,11 +1723,8 @@ all_nurses.each do |n|
|
|
1686
1723
|
model.add(num_shifts_worked >= min_shifts_per_nurse)
|
1687
1724
|
model.add(num_shifts_worked <= max_shifts_per_nurse)
|
1688
1725
|
end
|
1689
|
-
```
|
1690
|
-
|
1691
|
-
Create a printer
|
1692
1726
|
|
1693
|
-
|
1727
|
+
# create a printer
|
1694
1728
|
class NursesPartialSolutionPrinter < ORTools::CpSolverSolutionCallback
|
1695
1729
|
attr_reader :solution_count
|
1696
1730
|
|
@@ -1727,11 +1761,8 @@ class NursesPartialSolutionPrinter < ORTools::CpSolverSolutionCallback
|
|
1727
1761
|
@solution_count += 1
|
1728
1762
|
end
|
1729
1763
|
end
|
1730
|
-
```
|
1731
1764
|
|
1732
|
-
|
1733
|
-
|
1734
|
-
```ruby
|
1765
|
+
# call the solver and display the results
|
1735
1766
|
solver = ORTools::CpSolver.new
|
1736
1767
|
a_few_solutions = 5.times.to_a
|
1737
1768
|
solution_printer = NursesPartialSolutionPrinter.new(
|
@@ -1747,7 +1778,110 @@ puts " - wall time : %f s" % solver.wall_time
|
|
1747
1778
|
puts " - solutions found : %i" % solution_printer.solution_count
|
1748
1779
|
```
|
1749
1780
|
|
1750
|
-
|
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
|
1751
1885
|
|
1752
1886
|
[Example](https://github.com/google/or-tools/blob/stable/examples/python/sudoku_sat.py)
|
1753
1887
|
|
@@ -1814,14 +1948,173 @@ end
|
|
1814
1948
|
# solve and print solution
|
1815
1949
|
solver = ORTools::CpSolver.new
|
1816
1950
|
status = solver.solve(model)
|
1817
|
-
if status == :
|
1951
|
+
if status == :optimal
|
1818
1952
|
line.each do |i|
|
1819
1953
|
p line.map { |j| solver.value(grid[[i, j]]) }
|
1820
1954
|
end
|
1821
1955
|
end
|
1822
1956
|
```
|
1823
1957
|
|
1824
|
-
|
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
|
1825
2118
|
|
1826
2119
|
[Example](https://pythonhosted.org/PuLP/CaseStudies/a_set_partitioning_problem.html)
|
1827
2120
|
|
@@ -1893,7 +2186,7 @@ To get started with development:
|
|
1893
2186
|
git clone https://github.com/ankane/or-tools.git
|
1894
2187
|
cd or-tools
|
1895
2188
|
bundle install
|
1896
|
-
bundle exec rake compile
|
2189
|
+
bundle exec rake compile
|
1897
2190
|
bundle exec rake test
|
1898
2191
|
```
|
1899
2192
|
|