10
10
explains how to have a set with a custom key.
11
11
12
12
Programmer: Erel Segal-Halevi
13
+ Eitan Lichtman added Minimize Distance from Avg Objective
13
14
"""
14
-
15
+ import math
15
16
from typing import List , Tuple , Callable , Iterator , Any
16
17
import numpy as np
17
18
import logging , time
19
+
18
20
from prtpy import objectives as obj , Binner , BinsArray
19
21
20
22
logger = logging .getLogger (__name__ )
21
23
22
24
23
-
24
25
def anytime (
25
- binner : Binner , numbins : int , items : List [any ],
26
+ binner : Binner , numbins : int , items : List [any ], relative_value : List [ any ] = None ,
26
27
objective : obj .Objective = obj .MinimizeDifference ,
27
- use_lower_bound : bool = True , # Prune branches whose lower bound (= optimistic value) is at least as large as the current minimum.
28
- use_fast_lower_bound : bool = True , # A faster lower bound, that does not create the branch at all. Useful for min-max and max-min objectives.
29
- use_heuristic_3 : bool = False , # An improved stopping condition, applicable for min-max only. Not very useful in experiments.
30
- use_set_of_seen_states : bool = True ,
28
+ use_lower_bound : bool = True ,
29
+ # Prune branches whose lower bound (= optimistic value) is at least as large as the current minimum.
30
+ use_fast_lower_bound : bool = True ,
31
+ # A faster lower bound, that does not create the branch at all. Useful for min-max max-min and min-dist-avg objectives.
32
+ use_heuristic_3 : bool = False ,
33
+ # An improved stopping condition, applicable for min-max only. Not very useful in experiments.
34
+ use_set_of_seen_states : bool = True ,
31
35
time_limit : float = np .inf ,
32
36
) -> Iterator :
33
37
"""
@@ -41,6 +45,10 @@ def anytime(
41
45
Bin #0: [6, 5, 4], sum=15.0
42
46
Bin #1: [8, 7], sum=15.0
43
47
48
+ >>> printbins(anytime(BinnerKeepingContents(), 2, [4,5,6,7,8], [0.3,0.7], objective=obj.MinimizeDistAvg))
49
+ Bin #0: [5, 4], sum=9.0
50
+ Bin #1: [8, 7, 6], sum=21.0
51
+
44
52
The following examples are based on:
45
53
Walter (2013), 'Comparing the minimum completion times of two longest-first scheduling-heuristics'.
46
54
>>> walter_numbers = [46, 39, 27, 26, 16, 13, 10]
@@ -56,6 +64,56 @@ def anytime(
56
64
Bin #0: [46, 10], sum=56.0
57
65
Bin #1: [27, 16, 13], sum=56.0
58
66
Bin #2: [39, 26], sum=65.0
67
+ >>> printbins(anytime(BinnerKeepingContents(), 3, walter_numbers, objective=obj.MinimizeDistAvg))
68
+ Bin #0: [39, 16], sum=55.0
69
+ Bin #1: [46, 13], sum=59.0
70
+ Bin #2: [27, 26, 10], sum=63.0
71
+ >>> printbins(anytime(BinnerKeepingContents(), 3, walter_numbers,[0.2,0.4,0.4], objective=obj.MinimizeDistAvg))
72
+ Bin #0: [27, 10], sum=37.0
73
+ Bin #1: [39, 16, 13], sum=68.0
74
+ Bin #2: [46, 26], sum=72.0
75
+
76
+ >>> printbins(anytime(BinnerKeepingContents(), 5, [460000000, 390000000, 270000000, 260000000, 160000000, 130000000, 100000000],[0.2,0.4,0.1,0.15,0.15], objective=obj.MinimizeDistAvg))
77
+ Bin #0: [390000000], sum=390000000.0
78
+ Bin #1: [460000000, 130000000, 100000000], sum=690000000.0
79
+ Bin #2: [160000000], sum=160000000.0
80
+ Bin #3: [260000000], sum=260000000.0
81
+ Bin #4: [270000000], sum=270000000.0
82
+
83
+
84
+ >>> printbins(anytime(BinnerKeepingContents(), 10, [115268834, 22638149, 35260669, 68111031, 13376625, 20835125, 179398684, 69888000, 94462800, 5100340, 27184906, 305371, 272847, 545681, 1680746, 763835, 763835], [0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1], objective=obj.MinimizeDistAvg))
85
+ Bin #0: [13376625, 5100340, 1680746, 763835, 763835, 545681, 305371, 272847], sum=22809280.0
86
+ Bin #1: [20835125], sum=20835125.0
87
+ Bin #2: [22638149], sum=22638149.0
88
+ Bin #3: [27184906], sum=27184906.0
89
+ Bin #4: [35260669], sum=35260669.0
90
+ Bin #5: [68111031], sum=68111031.0
91
+ Bin #6: [69888000], sum=69888000.0
92
+ Bin #7: [94462800], sum=94462800.0
93
+ Bin #8: [115268834], sum=115268834.0
94
+ Bin #9: [179398684], sum=179398684.0
95
+
96
+
97
+ >>> printbins(anytime(BinnerKeepingContents(), 3, walter_numbers,[0.1,0.9,0], objective=obj.MinimizeDistAvg))
98
+ Bin #0: [16], sum=16.0
99
+ Bin #1: [46, 39, 27, 26, 13, 10], sum=161.0
100
+ Bin #2: [], sum=0.0
101
+
102
+
103
+ >>> printbins(anytime(BinnerKeepingContents(), 5, [2,2,5,5,5,5,9], objective=obj.MinimizeDistAvg))
104
+ Bin #0: [5], sum=5.0
105
+ Bin #1: [5], sum=5.0
106
+ Bin #2: [5, 2], sum=7.0
107
+ Bin #3: [5, 2], sum=7.0
108
+ Bin #4: [9], sum=9.0
109
+ >>> printbins(anytime(BinnerKeepingContents(), 3, [1,1,1,1], objective=obj.MinimizeDistAvg))
110
+ Bin #0: [1], sum=1.0
111
+ Bin #1: [1], sum=1.0
112
+ Bin #2: [1, 1], sum=2.0
113
+ >>> printbins(anytime(BinnerKeepingContents(), 3, [1,1,1,1,1], objective=obj.MinimizeDistAvg))
114
+ Bin #0: [1], sum=1.0
115
+ Bin #1: [1, 1], sum=2.0
116
+ Bin #2: [1, 1], sum=2.0
59
117
60
118
Compare results with and without the lower bound:
61
119
>>> random_numbers = np.random.randint(1, 2**48-1, 10, dtype=np.int64)
@@ -95,24 +153,34 @@ def anytime(
95
153
end_time = start_time + time_limit
96
154
97
155
sorted_items = sorted (items , key = binner .valueof , reverse = True )
98
- sums_of_remaining_items = [sum (map (binner .valueof , sorted_items [i :])) for i in range (numitems )] + [0 ] # For Heuristic 3
156
+ sums_of_remaining_items = [sum (map (binner .valueof , sorted_items [i :])) for i in range (numitems )] + [
157
+ 0 ] # For Heuristic 3
158
+ from prtpy import BinnerKeepingContents , BinnerKeepingSums , printbins
99
159
best_bins , best_objective_value = None , np .inf
100
160
101
- global_lower_bound = objective .lower_bound (np .zeros (numbins ), sums_of_remaining_items [0 ], are_sums_in_ascending_order = True )
102
161
103
- logger .info ("\n Complete Greedy %s Partitioning of %d items into %d parts. Lower bound: %s" , objective , numitems , numbins , global_lower_bound )
162
+ global_lower_bound = objective .lower_bound (np .zeros (numbins ), sums_of_remaining_items [0 ],
163
+ are_sums_in_ascending_order = True )
164
+
165
+ logger .info ("\n Complete Greedy %s Partitioning of %d items into %d parts. Lower bound: %s" , objective , numitems ,
166
+ numbins , global_lower_bound )
104
167
105
168
# Create a stack whose elements are a partition and the current depth.
106
169
# Initially, it contains a single tuple: an empty partition with depth 0.
107
- first_bins = binner .new_bins (numbins )
170
+ # If the input has relative values for each bin -
171
+ # we add a sum to each bin in order to equal them out to the bin with the highest relative value
172
+ # (at the end of the algorithm we will remove these sums).
173
+ first_bins = binner .new_bins (numbins )
174
+ if (relative_value ):
175
+ for i in range (numbins ):
176
+ binner .add_item_to_bin (first_bins , (max (relative_value ) * sum (items ) - relative_value [i ] * sum (items )), i )
108
177
first_vertex = (first_bins , 0 )
109
178
stack : List [Tuple [BinsArray , int ]] = [first_vertex ]
110
179
if use_set_of_seen_states :
111
180
seen_states = set (tuple (binner .sums (first_bins )))
112
-
113
181
# For logging and profiling:
114
- complete_partitions_checked = 0
115
- intermediate_partitions_checked = 1
182
+ complete_partitions_checked = 0
183
+ intermediate_partitions_checked = 1
116
184
117
185
times_fast_lower_bound_activated = 0
118
186
times_lower_bound_activated = 0
@@ -134,108 +202,133 @@ def anytime(
134
202
if new_objective_value < best_objective_value :
135
203
best_bins , best_objective_value = current_bins , new_objective_value
136
204
logger .info (" Found a better solution: %s, with value %s" , current_bins , best_objective_value )
137
- if new_objective_value <= global_lower_bound :
205
+ if new_objective_value <= global_lower_bound :
138
206
logger .info (" Solution matches global lower bound - stopping" )
139
207
break
140
208
continue
141
-
142
209
# Heuristic 3: "If the sum of the remaining unassigned integers plus the smallest current subset sum is <= the largest subset sum, all remaining integers are assigned to the subset with the smallest sum, terminating that branch of the tree."
143
210
# Note that this heuristic is valid only for the objective "minimize largest sum"!
144
- if use_heuristic_3 and objective == obj .MinimizeLargestSum :
211
+ if use_heuristic_3 and objective == obj .MinimizeLargestSum :
145
212
if sums_of_remaining_items [depth ] + current_sums [0 ] <= current_sums [- 1 ]:
146
213
new_bins = binner .copy_bins (current_bins )
147
- for i in range (depth ,numitems ):
214
+ for i in range (depth , numitems ):
148
215
binner .add_item_to_bin (new_bins , sorted_items [i ], 0 )
149
- binner .sort_by_ascending_sum (new_bins )
216
+ binner .sort_by_ascending_sum (new_bins )
150
217
new_depth = numitems
151
218
stack .append ((new_bins , new_depth ))
152
219
logger .debug (" Heuristic 3 activated" )
153
- times_heuristic_3_activated += 1
220
+ times_heuristic_3_activated += 1
154
221
continue
155
-
156
222
next_item = sorted_items [depth ]
157
- sum_of_remaining_items = sums_of_remaining_items [depth + 1 ]
223
+ sum_of_remaining_items = sums_of_remaining_items [depth + 1 ]
158
224
159
225
previous_bin_sum = None
160
226
161
227
# We want to insert the next item to the bin with the *smallest* sum first.
162
228
# But, since we use a stack, we have to insert it to the bin with the *largest* sum first,
163
229
# so that it is pushed deeper into the stack.
164
230
# Therefore, we proceed in reverse, by *descending* order of sum.
165
- for bin_index in reversed (range (numbins )):
231
+ for bin_index in reversed (range (numbins )):
166
232
167
233
# Heuristic 1: "If there are two subsets with the same sum, the current number is assigned to only one."
168
234
current_bin_sum = current_sums [bin_index ]
169
235
if current_bin_sum == previous_bin_sum :
170
- continue
236
+ continue
171
237
previous_bin_sum = current_bin_sum
172
238
173
239
# Fast-lower-bound heuristic - before creating the new vertex.
174
- # Currently implemented only for two objectives: min-max and max-min.
240
+ # Currently implemented only for two objectives: min-max, max-min and min-dist-avg
175
241
if use_fast_lower_bound :
176
- if objective == obj .MinimizeLargestSum :
242
+ if objective == obj .MinimizeLargestSum :
177
243
# "If an assignment to a subset creates a subset sum that equals or exceeds the largest subset sum in the best complete solution found so far, that branch is pruned from the tree."
178
244
fast_lower_bound = max (current_bin_sum + binner .valueof (next_item ), current_sums [- 1 ])
179
- elif objective == obj .MaximizeSmallestSum :
245
+ elif objective == obj .MaximizeSmallestSum :
180
246
# An adaptation of the above heuristic to maximizing the smallest sum.
181
- if bin_index == 0 :
182
- new_smallest_sum = min (current_sums [0 ]+ binner .valueof (next_item ), current_sums [1 ])
247
+ if bin_index == 0 :
248
+ new_smallest_sum = min (current_sums [0 ] + binner .valueof (next_item ), current_sums [1 ])
183
249
else :
184
250
new_smallest_sum = current_sums [0 ]
185
- fast_lower_bound = - (new_smallest_sum + sum_of_remaining_items )
251
+ fast_lower_bound = - (new_smallest_sum + sum_of_remaining_items )
252
+ elif objective == obj .MinimizeDistAvg :
253
+ if relative_value :
254
+ fast_lower_bound = 0
255
+ for i in range (numbins ):
256
+ # For each bin: we take off the sum that we added in the beginning of the algorithm (max(relative_value) * sum(items) - relative_value[i] * sum(items))
257
+ # Then we check if the difference between the bin's sum and the relative AVG for bin i: (sum(items)*relative_value[i])
258
+ # is positive and contributes to our final difference or negative and we will not add anything to our difference.
259
+ fast_lower_bound = fast_lower_bound + max ((current_sums [i ]- (max (relative_value ) * sum (items ) - relative_value [i ] * sum (items )))- sum (items )* relative_value [i ],0 )
260
+ else :
261
+ fast_lower_bound = 0
262
+ avg = sum (items ) / numbins
263
+ for i in range (numbins ):
264
+ fast_lower_bound = fast_lower_bound + max (current_sums [i ]- avg ,0 )
186
265
else :
187
266
fast_lower_bound = - np .inf
188
267
if fast_lower_bound >= best_objective_value :
189
268
times_fast_lower_bound_activated += 1
190
269
continue
191
270
192
271
new_bins = binner .add_item_to_bin (binner .copy_bins (current_bins ), next_item , bin_index )
193
- binner .sort_by_ascending_sum (new_bins )
272
+ if not relative_value :
273
+ binner .sort_by_ascending_sum (new_bins )
194
274
new_sums = tuple (binner .sums (new_bins ))
195
275
196
276
# Lower-bound heuristic.
197
277
if use_lower_bound :
198
- lower_bound = objective .lower_bound (new_sums , sum_of_remaining_items , are_sums_in_ascending_order = True )
278
+ lower_bound = objective .lower_bound (new_sums , sum_of_remaining_items , are_sums_in_ascending_order = False )
199
279
if lower_bound >= best_objective_value :
200
280
logger .debug (" Lower bound %f too large" , lower_bound )
201
281
times_lower_bound_activated += 1
202
282
continue
203
- if use_set_of_seen_states :
283
+ if use_set_of_seen_states :
204
284
if new_sums in seen_states :
205
285
logger .debug (" State %s already seen" , new_sums )
206
286
times_seen_state_skipped += 1
207
287
continue
208
- seen_states .add (new_sums ) # should be after if use_lower_bound
288
+ seen_states .add (new_sums ) # should be after if use_lower_bound
209
289
210
290
new_vertex = (new_bins , depth + 1 )
211
291
stack .append (new_vertex )
212
292
intermediate_partitions_checked += 1
213
293
214
- logger .info ("Checked %d out of %d complete partitions, and %d intermediate partitions." , complete_partitions_checked , numbins ** numitems , intermediate_partitions_checked )
215
- logger .info (" Heuristics: fast lower bound = %d, lower bound = %d, seen state = %d, heuristic 3 = %d." , times_fast_lower_bound_activated , times_lower_bound_activated , times_seen_state_skipped , times_heuristic_3_activated )
294
+ logger .info ("Checked %d out of %d complete partitions, and %d intermediate partitions." ,
295
+ complete_partitions_checked , numbins ** numitems , intermediate_partitions_checked )
296
+ logger .info (" Heuristics: fast lower bound = %d, lower bound = %d, seen state = %d, heuristic 3 = %d." ,
297
+ times_fast_lower_bound_activated , times_lower_bound_activated , times_seen_state_skipped ,
298
+ times_heuristic_3_activated )
216
299
300
+
301
+ if (relative_value ):
302
+ # For each bin we remove the value that we added in the beginning of the algorithm.
303
+ for i in range (numbins ):
304
+ binner .remove_item_from_bin (best_bins , i , 0 )
305
+
306
+ for i in range (numbins ):
307
+ binner .sums (best_bins )[i ] = math .floor (binner .sums (best_bins )[i ])
217
308
return best_bins
218
309
219
310
220
311
if __name__ == "__main__" :
221
312
import doctest , sys
313
+
222
314
(failures , tests ) = doctest .testmod (report = True , optionflags = doctest .FAIL_FAST )
223
315
print ("{} failures, {} tests" .format (failures , tests ))
224
- if failures > 0 :
316
+ if failures > 0 :
225
317
sys .exit ()
226
-
318
+
227
319
# DEMO
228
320
logger .setLevel (logging .INFO )
229
321
logger .addHandler (logging .StreamHandler ())
230
322
231
323
from prtpy import BinnerKeepingContents , BinnerKeepingSums
232
- anytime (BinnerKeepingContents (), 2 , [4 ,5 ,6 ,7 ,8 ], objective = obj .MinimizeLargestSum )
324
+
325
+ anytime (BinnerKeepingContents (), 2 , [4 , 5 , 6 , 7 , 8 ], objective = obj .MinimizeLargestSum )
233
326
234
327
walter_numbers = [46 , 39 , 27 , 26 , 16 , 13 , 10 ]
235
328
anytime (BinnerKeepingContents (), 3 , walter_numbers , objective = obj .MaximizeSmallestSum )
236
329
anytime (BinnerKeepingContents (), 3 , walter_numbers , objective = obj .MinimizeLargestSum )
237
330
238
- random_numbers = np .random .randint (1 , 2 ** 16 - 1 , 15 , dtype = np .int64 )
331
+ random_numbers = np .random .randint (1 , 2 ** 16 - 1 , 15 , dtype = np .int64 )
239
332
anytime (BinnerKeepingSums (), 3 , random_numbers , objective = obj .MaximizeSmallestSum )
240
333
anytime (BinnerKeepingSums (), 3 , random_numbers , objective = obj .MinimizeLargestSum )
241
334
anytime (BinnerKeepingSums (), 3 , random_numbers , objective = obj .MinimizeDifference )
0 commit comments