or-tools 0.1.4 → 0.3.2

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