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