or-tools 0.1.4 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76a0be1ca2f3a5cc29b1bda79e48743ec4357e71dff41f6351666c41f068a958
4
- data.tar.gz: 8071733ad4ee71793d9e04b17594e293cab8ddf97d549afad512de59ab511e1e
3
+ metadata.gz: ae2a270bed20b9d1236296f7368506b7ddb2172470ab2d57c655340f2c3a7a13
4
+ data.tar.gz: ef2e7101057762004b6bbb4f72b3dd5da50bc3cbfa236673f87b4b55d8aff901
5
5
  SHA512:
6
- metadata.gz: 88d74bafb8fd8300eef1163defb18c09db9e7cee3914b1ca983b76cc4bae51346c070d150618c575c76f8634008b8b3fc9398d052c586a97354926537184680a
7
- data.tar.gz: 253000a4a5e5c7dc0aed1ee5796168d5ecf8e9bf6158998c274d2aa3c81c1d43c2c8626ec26471191ccc2cad542faf5a7d0ba5fa4447f624739a874012fa6fa0
6
+ metadata.gz: 534ac8c95ae51699667d0300f2d61664a28c8f992392a6621e70b47bae500fab41a9c30321a8a57f90a58abc5a740cb31c796e7f0447fa3d191161dd764f7467
7
+ data.tar.gz: 2418aec568f5ae54883dd2882ea961e79ace65d889dd1442cadfd11a2782fae965f8e18298e851794ac4e0a0e3b2f8c82ff3febc38e112016a2cb2236febf656
@@ -1,3 +1,28 @@
1
+ ## 0.3.2 (2020-08-04)
2
+
3
+ - Updated OR-Tools to 7.8
4
+ - Added binary installation for Ubuntu 20.04
5
+
6
+ ## 0.3.1 (2020-07-21)
7
+
8
+ - Reduced gem size
9
+
10
+ ## 0.3.0 (2020-07-21)
11
+
12
+ - Updated OR-Tools to 7.7
13
+ - Added `BasicScheduler` class
14
+ - Added `Seating` class
15
+ - Added `TSP` class
16
+ - Added `Sudoku` class
17
+
18
+ ## 0.2.0 (2020-05-22)
19
+
20
+ - No longer need to download the OR-Tools C++ library separately on Mac, Ubuntu 18.04, Ubuntu 16.04, Debian 10, and CentOS 8
21
+
22
+ ## 0.1.5 (2020-04-23)
23
+
24
+ - Added support for OR-Tools 7.6
25
+
1
26
  ## 0.1.4 (2020-04-19)
2
27
 
3
28
  - Added support for the Job Shop Problem
data/README.md CHANGED
@@ -6,24 +6,261 @@
6
6
 
7
7
  ## Installation
8
8
 
9
- Download the [OR-Tools C++ library](https://developers.google.com/optimization/install/cpp). Then run:
9
+ Add this line to your application’s Gemfile:
10
10
 
11
- ```sh
12
- bundle config build.or-tools --with-or-tools-dir=/path/to/or-tools
11
+ ```ruby
12
+ gem 'or-tools'
13
13
  ```
14
14
 
15
- Add this line to your application’s Gemfile:
15
+ Installation can take a few minutes as OR-Tools downloads and builds.
16
+
17
+ ## Higher Level Interfaces
18
+
19
+ - [Scheduling](#scheduling)
20
+ - [Seating](#seating)
21
+ - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
22
+ - [Sudoku](#sudoku)
23
+
24
+ ### Scheduling
25
+
26
+ Specify people and their availabililty
16
27
 
17
28
  ```ruby
18
- gem 'or-tools'
29
+ people = [
30
+ {
31
+ availability: [
32
+ {starts_at: Time.parse("2020-01-01 08:00:00"), ends_at: Time.parse("2020-01-01 16:00:00")},
33
+ {starts_at: Time.parse("2020-01-02 08:00:00"), ends_at: Time.parse("2020-01-02 16:00:00")}
34
+ ],
35
+ max_hours: 40 # optional, applies to entire scheduling period
36
+ },
37
+ {
38
+ availability: [
39
+ {starts_at: Time.parse("2020-01-01 08:00:00"), ends_at: Time.parse("2020-01-01 16:00:00")},
40
+ {starts_at: Time.parse("2020-01-03 08:00:00"), ends_at: Time.parse("2020-01-03 16:00:00")}
41
+ ],
42
+ max_hours: 20
43
+ }
44
+ ]
45
+ ```
46
+
47
+ Specify shifts
48
+
49
+ ```ruby
50
+ shifts = [
51
+ {starts_at: Time.parse("2020-01-01 08:00:00"), ends_at: Time.parse("2020-01-01 16:00:00")},
52
+ {starts_at: Time.parse("2020-01-02 08:00:00"), ends_at: Time.parse("2020-01-02 16:00:00")},
53
+ {starts_at: Time.parse("2020-01-03 08:00:00"), ends_at: Time.parse("2020-01-03 16:00:00")}
54
+ ]
55
+ ```
56
+
57
+ Run the scheduler
58
+
59
+ ```ruby
60
+ scheduler = ORTools::BasicScheduler.new(people: people, shifts: shifts)
61
+ ```
62
+
63
+ The scheduler maximizes the number of assigned hours. A person must be available for the entire shift to be considered for it.
64
+
65
+ Get assignments (returns indexes of people and shifts)
66
+
67
+ ```ruby
68
+ scheduler.assignments
69
+ # [
70
+ # {person: 2, shift: 0},
71
+ # {person: 0, shift: 1},
72
+ # {person: 1, shift: 2}
73
+ # ]
74
+ ```
75
+
76
+ Get assigned hours and total hours
77
+
78
+ ```ruby
79
+ scheduler.assigned_hours
80
+ scheduler.total_hours
81
+ ```
82
+
83
+ Feel free to create an issue if you have a scheduling use case that’s not covered.
84
+
85
+ ### Seating
86
+
87
+ Create a seating chart based on personal connections. Uses [this approach](https://www.improbable.com/news/2012/Optimal-seating-chart.pdf).
88
+
89
+ Specify connections
90
+
91
+ ```ruby
92
+ connections = [
93
+ {people: ["A", "B", "C"], weight: 2},
94
+ {people: ["C", "D", "E", "F"], weight: 1}
95
+ ]
96
+ ```
97
+
98
+ Use different weights to prioritize seating. For a wedding, it may look like:
99
+
100
+ ```ruby
101
+ connections = [
102
+ {people: knows_partner1, weight: 1},
103
+ {people: knows_partner2, weight: 1},
104
+ {people: relationship1, weight: 100},
105
+ {people: relationship2, weight: 100},
106
+ {people: relationship3, weight: 100},
107
+ {people: friend_group1, weight: 10},
108
+ {people: friend_group2, weight: 10},
109
+ # ...
110
+ ]
111
+ ```
112
+
113
+ If two people have multiple connections, weights are added.
114
+
115
+ Specify tables and their capacity
116
+
117
+ ```ruby
118
+ tables = [3, 3]
119
+ ```
120
+
121
+ Assign seats
122
+
123
+ ```ruby
124
+ seating = ORTools::Seating.new(connections: connections, tables: tables)
125
+ ```
126
+
127
+ Each person will have a connection with at least one other person at their table.
128
+
129
+ Get tables
130
+
131
+ ```ruby
132
+ seating.assigned_tables
133
+ ```
134
+
135
+ Get assignments by person
136
+
137
+ ```ruby
138
+ seating.assignments
139
+ ```
140
+
141
+ Get all connections for a person
142
+
143
+ ```ruby
144
+ seating.connections_for(person)
145
+ ```
146
+
147
+ Get connections for a person at their table
148
+
149
+ ```ruby
150
+ seating.connections_for(person, same_table: true)
151
+ ```
152
+
153
+ ### Traveling Salesperson Problem (TSP)
154
+
155
+ Create locations - the first location will be the starting and ending point
156
+
157
+ ```ruby
158
+ locations = [
159
+ {name: "Tokyo", latitude: 35.6762, longitude: 139.6503},
160
+ {name: "Delhi", latitude: 28.7041, longitude: 77.1025},
161
+ {name: "Shanghai", latitude: 31.2304, longitude: 121.4737},
162
+ {name: "São Paulo", latitude: -23.5505, longitude: -46.6333},
163
+ {name: "Mexico City", latitude: 19.4326, longitude: -99.1332},
164
+ {name: "Cairo", latitude: 30.0444, longitude: 31.2357},
165
+ {name: "Mumbai", latitude: 19.0760, longitude: 72.8777},
166
+ {name: "Beijing", latitude: 39.9042, longitude: 116.4074},
167
+ {name: "Dhaka", latitude: 23.8103, longitude: 90.4125},
168
+ {name: "Osaka", latitude: 34.6937, longitude: 135.5023},
169
+ {name: "New York City", latitude: 40.7128, longitude: -74.0060},
170
+ {name: "Karachi", latitude: 24.8607, longitude: 67.0011},
171
+ {name: "Buenos Aires", latitude: -34.6037, longitude: -58.3816}
172
+ ]
173
+ ```
174
+
175
+ Locations can have any fields - only `latitude` and `longitude` are required
176
+
177
+ Get route
178
+
179
+ ```ruby
180
+ tsp = ORTools::TSP.new(locations)
181
+ tsp.route # [{name: "Tokyo", ...}, {name: "Osaka", ...}, ...]
182
+ ```
183
+
184
+ Get distances between locations on route
185
+
186
+ ```ruby
187
+ tsp.distances # [392.441, 1362.926, 1067.31, ...]
188
+ ```
189
+
190
+ Distances are in kilometers - multiply by `0.6214` for miles
191
+
192
+ Get total distance
193
+
194
+ ```ruby
195
+ tsp.total_distance
19
196
  ```
20
197
 
21
- ## Getting Started
198
+ ### Sudoku
199
+
200
+ Create a puzzle with zeros in empty cells
201
+
202
+ ```ruby
203
+ grid = [
204
+ [0, 6, 0, 0, 5, 0, 0, 2, 0],
205
+ [0, 0, 0, 3, 0, 0, 0, 9, 0],
206
+ [7, 0, 0, 6, 0, 0, 0, 1, 0],
207
+ [0, 0, 6, 0, 3, 0, 4, 0, 0],
208
+ [0, 0, 4, 0, 7, 0, 1, 0, 0],
209
+ [0, 0, 5, 0, 9, 0, 8, 0, 0],
210
+ [0, 4, 0, 0, 0, 1, 0, 0, 6],
211
+ [0, 3, 0, 0, 0, 8, 0, 0, 0],
212
+ [0, 2, 0, 0, 4, 0, 0, 5, 0]
213
+ ]
214
+ sudoku = ORTools::Sudoku.new(grid)
215
+ sudoku.solution
216
+ ```
217
+
218
+ It can also solve more advanced puzzles like [The Miracle](https://www.youtube.com/watch?v=yKf9aUIxdb4)
219
+
220
+ ```ruby
221
+ grid = [
222
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
223
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
224
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
225
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
226
+ [0, 0, 1, 0, 0, 0, 0, 0, 0],
227
+ [0, 0, 0, 0, 0, 0, 2, 0, 0],
228
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
229
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
230
+ [0, 0, 0, 0, 0, 0, 0, 0, 0]
231
+ ]
232
+ sudoku = ORTools::Sudoku.new(grid, anti_knight: true, anti_king: true, non_consecutive: true)
233
+ sudoku.solution
234
+ ```
235
+
236
+ And [this 4-digit puzzle](https://www.youtube.com/watch?v=hAyZ9K2EBF0)
237
+
238
+ ```ruby
239
+ grid = [
240
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
241
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
242
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
243
+ [3, 8, 4, 0, 0, 0, 0, 0, 0],
244
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
245
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
246
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
247
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
248
+ [0, 0, 0, 0, 0, 0, 0, 0, 2]
249
+ ]
250
+ sudoku = ORTools::Sudoku.new(grid, x: true, anti_knight: true, magic_square: true)
251
+ sudoku.solution
252
+ ```
253
+
254
+ ## Guides
22
255
 
23
256
  Linear Optimization
24
257
 
25
258
  - [The Glop Linear Solver](#the-glop-linear-solver)
26
259
 
260
+ Integer Optimization
261
+
262
+ - [Mixed-Integer Programming](#mixed-integer-programming)
263
+
27
264
  Constraint Optimization
28
265
 
29
266
  - [CP-SAT Solver](#cp-sat-solver)
@@ -31,13 +268,14 @@ Constraint Optimization
31
268
  - [Cryptarithmetic](#cryptarithmetic)
32
269
  - [The N-queens Problem](#the-n-queens-problem)
33
270
 
34
- Integer Optimization
271
+ Assignment
35
272
 
36
- - [Mixed-Integer Programming](#mixed-integer-programming)
273
+ - [Assignment](#assignment)
274
+ - [Assignment with Teams](#assignment-with-teams)
37
275
 
38
276
  Routing
39
277
 
40
- - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
278
+ - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp-1)
41
279
  - [Vehicle Routing Problem (VRP)](#vehicle-routing-problem-vrp)
42
280
  - [Capacity Constraints](#capacity-constraints)
43
281
  - [Pickups and Deliveries](#pickups-and-deliveries)
@@ -56,12 +294,7 @@ Network Flows
56
294
 
57
295
  - [Maximum Flows](#maximum-flows)
58
296
  - [Minimum Cost Flows](#minimum-cost-flows)
59
-
60
- Assignment
61
-
62
- - [Assignment](#assignment)
63
- - [Assignment as a Min Cost Problem](#assignment-as-a-min-cost-problem)
64
- - [Assignment as a MIP Problem](#assignment-as-a-mip-problem)
297
+ - [Assignment as a Min Cost Flow Problem](#assignment-as-a-min-cost-flow-problem)
65
298
 
66
299
  Scheduling
67
300
 
@@ -70,7 +303,7 @@ Scheduling
70
303
 
71
304
  Other Examples
72
305
 
73
- - [Sudoku](#sudoku)
306
+ - [Sudoku](#sudoku-1)
74
307
  - [Wedding Seating Chart](#wedding-seating-chart)
75
308
  - [Set Partitioning](#set-partitioning)
76
309
 
@@ -118,6 +351,52 @@ puts "y = #{y.solution_value}"
118
351
  puts "Optimal objective value = #{opt_solution}"
119
352
  ```
120
353
 
354
+ ### Mixed-Integer Programming
355
+
356
+ [Guide](https://developers.google.com/optimization/mip/integer_opt)
357
+
358
+ ```ruby
359
+ # declare the MIP solver
360
+ solver = ORTools::Solver.new("simple_mip_program", :cbc)
361
+
362
+ # define the variables
363
+ infinity = solver.infinity
364
+ x = solver.int_var(0.0, infinity, "x")
365
+ y = solver.int_var(0.0, infinity, "y")
366
+
367
+ puts "Number of variables = #{solver.num_variables}"
368
+
369
+ # define the constraints
370
+ c0 = solver.constraint(-infinity, 17.5)
371
+ c0.set_coefficient(x, 1)
372
+ c0.set_coefficient(y, 7)
373
+
374
+ c1 = solver.constraint(-infinity, 3.5)
375
+ c1.set_coefficient(x, 1);
376
+ c1.set_coefficient(y, 0);
377
+
378
+ puts "Number of constraints = #{solver.num_constraints}"
379
+
380
+ # define the objective
381
+ objective = solver.objective
382
+ objective.set_coefficient(x, 1)
383
+ objective.set_coefficient(y, 10)
384
+ objective.set_maximization
385
+
386
+ # call the solver
387
+ status = solver.solve
388
+
389
+ # display the solution
390
+ if status == :optimal
391
+ puts "Solution:"
392
+ puts "Objective value = #{solver.objective.value}"
393
+ puts "x = #{x.solution_value}"
394
+ puts "y = #{y.solution_value}"
395
+ else
396
+ puts "The problem does not have an optimal solution."
397
+ end
398
+ ```
399
+
121
400
  ### CP-SAT Solver
122
401
 
123
402
  [Guide](https://developers.google.com/optimization/cp/cp_solver)
@@ -140,7 +419,7 @@ solver = ORTools::CpSolver.new
140
419
  status = solver.solve(model)
141
420
 
142
421
  # display the first solution
143
- if status == :feasible
422
+ if status == :optimal
144
423
  puts "x = #{solver.value(x)}"
145
424
  puts "y = #{solver.value(y)}"
146
425
  puts "z = #{solver.value(z)}"
@@ -299,50 +578,119 @@ puts
299
578
  puts "Solutions found : %i" % solution_printer.solution_count
300
579
  ```
301
580
 
302
- ### Mixed-Integer Programming
303
581
 
304
- [Guide](https://developers.google.com/optimization/mip/integer_opt)
582
+ ### Assignment
583
+
584
+ [Guide](https://developers.google.com/optimization/assignment/assignment_example)
305
585
 
306
586
  ```ruby
307
- # declare the MIP solver
308
- solver = ORTools::Solver.new("simple_mip_program", :cbc)
587
+ # create the data
588
+ cost = [[ 90, 76, 75, 70],
589
+ [ 35, 85, 55, 65],
590
+ [125, 95, 90, 105],
591
+ [ 45, 110, 95, 115]]
309
592
 
310
- # define the variables
311
- infinity = solver.infinity
312
- x = solver.int_var(0.0, infinity, "x")
313
- y = solver.int_var(0.0, infinity, "y")
593
+ rows = cost.length
594
+ cols = cost[0].length
314
595
 
315
- puts "Number of variables = #{solver.num_variables}"
596
+ # create the solver
597
+ assignment = ORTools::LinearSumAssignment.new
316
598
 
317
- # define the constraints
318
- c0 = solver.constraint(-infinity, 17.5)
319
- c0.set_coefficient(x, 1)
320
- c0.set_coefficient(y, 7)
599
+ # add the costs to the solver
600
+ rows.times do |worker|
601
+ cols.times do |task|
602
+ if cost[worker][task]
603
+ assignment.add_arc_with_cost(worker, task, cost[worker][task])
604
+ end
605
+ end
606
+ end
321
607
 
322
- c1 = solver.constraint(-infinity, 3.5)
323
- c1.set_coefficient(x, 1);
324
- c1.set_coefficient(y, 0);
608
+ # invoke the solver
609
+ solve_status = assignment.solve
610
+ if solve_status == :optimal
611
+ puts "Total cost = #{assignment.optimal_cost}"
612
+ puts
613
+ assignment.num_nodes.times do |i|
614
+ puts "Worker %d assigned to task %d. Cost = %d" % [
615
+ i,
616
+ assignment.right_mate(i),
617
+ assignment.assignment_cost(i)
618
+ ]
619
+ end
620
+ elsif solve_status == :infeasible
621
+ puts "No assignment is possible."
622
+ elsif solve_status == :possible_overflow
623
+ puts "Some input costs are too large and may cause an integer overflow."
624
+ end
625
+ ```
325
626
 
326
- puts "Number of constraints = #{solver.num_constraints}"
627
+ ### Assignment with Teams
327
628
 
328
- # define the objective
329
- objective = solver.objective
330
- objective.set_coefficient(x, 1)
331
- objective.set_coefficient(y, 10)
332
- objective.set_maximization
629
+ [Guide](https://developers.google.com/optimization/assignment/assignment_teams)
333
630
 
334
- # call the solver
335
- status = solver.solve
631
+ ```ruby
632
+ # create the solver
633
+ solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
336
634
 
337
- # display the solution
338
- if status == :optimal
339
- puts "Solution:"
340
- puts "Objective value = #{solver.objective.value}"
341
- puts "x = #{x.solution_value}"
342
- puts "y = #{y.solution_value}"
343
- else
344
- puts "The problem does not have an optimal solution."
635
+ # create the data
636
+ cost = [[90, 76, 75, 70],
637
+ [35, 85, 55, 65],
638
+ [125, 95, 90, 105],
639
+ [45, 110, 95, 115],
640
+ [60, 105, 80, 75],
641
+ [45, 65, 110, 95]]
642
+
643
+ team1 = [0, 2, 4]
644
+ team2 = [1, 3, 5]
645
+ team_max = 2
646
+
647
+ # create the variables
648
+ num_workers = cost.length
649
+ num_tasks = cost[1].length
650
+ x = {}
651
+
652
+ num_workers.times do |i|
653
+ num_tasks.times do |j|
654
+ x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
655
+ end
656
+ end
657
+
658
+ # create the objective function
659
+ solver.minimize(solver.sum(
660
+ num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
661
+ ))
662
+
663
+ # create the constraints
664
+ num_workers.times do |i|
665
+ solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
666
+ end
667
+
668
+ num_tasks.times do |j|
669
+ solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
670
+ end
671
+
672
+ solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
673
+ solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
674
+
675
+ # invoke the solver
676
+ sol = solver.solve
677
+
678
+ puts "Total cost = #{solver.objective.value}"
679
+ puts
680
+ num_workers.times do |i|
681
+ num_tasks.times do |j|
682
+ if x[[i, j]].solution_value > 0
683
+ puts "Worker %d assigned to task %d. Cost = %d" % [
684
+ i,
685
+ j,
686
+ cost[i][j]
687
+ ]
688
+ end
689
+ end
345
690
  end
691
+
692
+ puts
693
+ puts "Time = #{solver.wall_time} milliseconds"
346
694
  ```
347
695
 
348
696
  ### Traveling Salesperson Problem (TSP)
@@ -384,7 +732,7 @@ transit_callback_index = routing.register_transit_callback(distance_callback)
384
732
  routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
385
733
 
386
734
  # run the solver
387
- assignment = routing.solve(first_solution_strategy: :path_cheaper_arc)
735
+ assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
388
736
 
389
737
  # print the solution
390
738
  puts "Objective: #{assignment.objective_value} miles"
@@ -1280,52 +1628,7 @@ else
1280
1628
  end
1281
1629
  ```
1282
1630
 
1283
- ### Assignment
1284
-
1285
- [Guide](https://developers.google.com/optimization/assignment/simple_assignment)
1286
-
1287
- ```ruby
1288
- # create the data
1289
- cost = [[ 90, 76, 75, 70],
1290
- [ 35, 85, 55, 65],
1291
- [125, 95, 90, 105],
1292
- [ 45, 110, 95, 115]]
1293
-
1294
- rows = cost.length
1295
- cols = cost[0].length
1296
-
1297
- # create the solver
1298
- assignment = ORTools::LinearSumAssignment.new
1299
-
1300
- # add the costs to the solver
1301
- rows.times do |worker|
1302
- cols.times do |task|
1303
- if cost[worker][task]
1304
- assignment.add_arc_with_cost(worker, task, cost[worker][task])
1305
- end
1306
- end
1307
- end
1308
-
1309
- # invoke the solver
1310
- solve_status = assignment.solve
1311
- if solve_status == :optimal
1312
- puts "Total cost = #{assignment.optimal_cost}"
1313
- puts
1314
- assignment.num_nodes.times do |i|
1315
- puts "Worker %d assigned to task %d. Cost = %d" % [
1316
- i,
1317
- assignment.right_mate(i),
1318
- assignment.assignment_cost(i)
1319
- ]
1320
- end
1321
- elsif solve_status == :infeasible
1322
- puts "No assignment is possible."
1323
- elsif solve_status == :possible_overflow
1324
- puts "Some input costs are too large and may cause an integer overflow."
1325
- end
1326
- ```
1327
-
1328
- ### Assignment as a Min Cost Problem
1631
+ ### Assignment as a Min Cost Flow Problem
1329
1632
 
1330
1633
  [Guide](https://developers.google.com/optimization/assignment/assignment_min_cost_flow)
1331
1634
 
@@ -1374,75 +1677,6 @@ else
1374
1677
  end
1375
1678
  ```
1376
1679
 
1377
- ### Assignment as a MIP Problem
1378
-
1379
- [Guide](https://developers.google.com/optimization/assignment/assignment_mip)
1380
-
1381
- ```ruby
1382
- # create the solver
1383
- solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
1384
-
1385
- # create the data
1386
- cost = [[90, 76, 75, 70],
1387
- [35, 85, 55, 65],
1388
- [125, 95, 90, 105],
1389
- [45, 110, 95, 115],
1390
- [60, 105, 80, 75],
1391
- [45, 65, 110, 95]]
1392
-
1393
- team1 = [0, 2, 4]
1394
- team2 = [1, 3, 5]
1395
- team_max = 2
1396
-
1397
- # create the variables
1398
- num_workers = cost.length
1399
- num_tasks = cost[1].length
1400
- x = {}
1401
-
1402
- num_workers.times do |i|
1403
- num_tasks.times do |j|
1404
- x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
1405
- end
1406
- end
1407
-
1408
- # create the objective function
1409
- solver.minimize(solver.sum(
1410
- num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
1411
- ))
1412
-
1413
- # create the constraints
1414
- num_workers.times do |i|
1415
- solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
1416
- end
1417
-
1418
- num_tasks.times do |j|
1419
- solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
1420
- end
1421
-
1422
- solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
1423
- solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
1424
-
1425
- # invoke the solver
1426
- sol = solver.solve
1427
-
1428
- puts "Total cost = #{solver.objective.value}"
1429
- puts
1430
- num_workers.times do |i|
1431
- num_tasks.times do |j|
1432
- if x[[i, j]].solution_value > 0
1433
- puts "Worker %d assigned to task %d. Cost = %d" % [
1434
- i,
1435
- j,
1436
- cost[i][j]
1437
- ]
1438
- end
1439
- end
1440
- end
1441
-
1442
- puts
1443
- puts "Time = #{solver.wall_time} milliseconds"
1444
- ```
1445
-
1446
1680
  ### Employee Scheduling
1447
1681
 
1448
1682
  [Guide](https://developers.google.com/optimization/scheduling/employee_scheduling)
@@ -1714,7 +1948,7 @@ end
1714
1948
  # solve and print solution
1715
1949
  solver = ORTools::CpSolver.new
1716
1950
  status = solver.solve(model)
1717
- if status == :feasible
1951
+ if status == :optimal
1718
1952
  line.each do |i|
1719
1953
  p line.map { |j| solver.value(grid[[i, j]]) }
1720
1954
  end
@@ -1729,8 +1963,8 @@ end
1729
1963
  # From
1730
1964
  # Meghan L. Bellows and J. D. Luc Peterson
1731
1965
  # "Finding an optimal seating chart for a wedding"
1732
- # http://www.improbable.com/news/2012/Optimal-seating-chart.pdf
1733
- # http://www.improbable.com/2012/02/12/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
1734
1968
  #
1735
1969
  # Every year, millions of brides (not to mention their mothers, future
1736
1970
  # mothers-in-law, and occasionally grooms) struggle with one of the
@@ -1803,19 +2037,17 @@ all_tables.each do |t|
1803
2037
  end
1804
2038
  end
1805
2039
 
2040
+ pairs = all_guests.combination(2)
2041
+
1806
2042
  colocated = {}
1807
- (num_guests - 1).times do |g1|
1808
- (g1 + 1).upto(num_guests - 1) do |g2|
1809
- colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
1810
- end
2043
+ pairs.each do |g1, g2|
2044
+ colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
1811
2045
  end
1812
2046
 
1813
2047
  same_table = {}
1814
- (num_guests - 1).times do |g1|
1815
- (g1 + 1).upto(num_guests - 1) do |g2|
1816
- all_tables.each do |t|
1817
- same_table[[g1, g2, t]] = model.new_bool_var("guest %i seats with guest %i on table %i" % [g1, g2, t])
1818
- end
2048
+ pairs.each do |g1, g2|
2049
+ all_tables.each do |t|
2050
+ same_table[[g1, g2, t]] = model.new_bool_var("guest %i seats with guest %i on table %i" % [g1, g2, t])
1819
2051
  end
1820
2052
  end
1821
2053
 
@@ -1837,29 +2069,32 @@ all_tables.each do |t|
1837
2069
  end
1838
2070
 
1839
2071
  # Link colocated with seats
1840
- (num_guests - 1).times do |g1|
1841
- (g1 + 1).upto(num_guests - 1) do |g2|
1842
- all_tables.each do |t|
1843
- # Link same_table and seats.
1844
- model.add_bool_or([seats[[t, g1]].not, seats[[t, g2]].not, same_table[[g1, g2, t]]])
1845
- model.add_implication(same_table[[g1, g2, t]], seats[[t, g1]])
1846
- model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])
1847
- end
1848
-
1849
- # Link colocated and same_table.
1850
- model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])
2072
+ pairs.each do |g1, g2|
2073
+ all_tables.each do |t|
2074
+ # Link same_table and seats.
2075
+ model.add_bool_or([seats[[t, g1]].not, seats[[t, g2]].not, same_table[[g1, g2, t]]])
2076
+ model.add_implication(same_table[[g1, g2, t]], seats[[t, g1]])
2077
+ model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])
1851
2078
  end
2079
+
2080
+ # Link colocated and same_table.
2081
+ model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])
1852
2082
  end
1853
2083
 
1854
2084
  # Min known neighbors rule.
1855
- all_tables.each do |t|
2085
+ all_guests.each do |g|
1856
2086
  model.add(
1857
2087
  model.sum(
1858
- (num_guests - 1).times.flat_map { |g1|
1859
- (g1 + 1).upto(num_guests - 1).select { |g2| c[g1][g2] > 0 }.flat_map { |g2|
1860
- all_tables.map { |t2| same_table[[g1, g2, t2]] }
1861
- }
1862
- }
2088
+ (g + 1).upto(num_guests - 1).
2089
+ select { |g2| c[g][g2] > 0 }.
2090
+ product(all_tables).
2091
+ map { |g2, t| same_table[[g, g2, t]] }
2092
+ ) +
2093
+ model.sum(
2094
+ g.times.
2095
+ select { |g1| c[g1][g] > 0 }.
2096
+ product(all_tables).
2097
+ map { |g1, t| same_table[[g1, g, t]] }
1863
2098
  ) >= min_known_neighbors
1864
2099
  )
1865
2100
  end
@@ -1951,7 +2186,7 @@ To get started with development:
1951
2186
  git clone https://github.com/ankane/or-tools.git
1952
2187
  cd or-tools
1953
2188
  bundle install
1954
- bundle exec rake compile -- --with-or-tools-dir=/path/to/or-tools
2189
+ bundle exec rake compile
1955
2190
  bundle exec rake test
1956
2191
  ```
1957
2192