use strict; use warnings; use List::Util 'shuffle'; # A lot could be done better ... (less globals, more structure, etc.) # - of course, I am in a hurry! ;-) my @a = qw( 121 182 111 160 105 113 121 97 123 157 133 161 141 135 137 145 133 137 151 118 126 141 174 181 154 109 198 114 122 162 91 99 116 122 195 199 150 192 163 88 112 157 182 210 124 105 144 166 144 257 164 156 173 154 193 142 143 126 118 130 107 86 131 154 131 147 134 118 115 135 141 158 129 143 126 128 134 129 167 130 135 117 127 146 96 117 99 99 139 152 149 136 105 124 136 160 160 139 177 115 123 103 150 183 132 171 121 114 111 113 131 144 122 141 111 139 145 109 114 122 103 160 153 147 172 155 122 296 124 112 161 124 311 99 157 122 120 198 152 140 162 177 98 138 156 177 103 180 187 173 150 135 168 132 196 112 195 126 113 116 105 116 151 216 188 158 121 166 148 132 89 197 92 115 98 130 103 120 261 143 126 167 203 95 165 129 ); #-- model data # my ($bin_no, $min_capacity, $max_capacity, $low, $high) = (38, 4,5, 694,750); # my ($bin_no, $min_capacity, $max_capacity, $low, $high) = (32, 5,6, 824,900); my ($bin_no, $min_capacity, $max_capacity, $low, $high) = (31, 6,6, 840,900); # my ($bin_no, $min_capacity, $max_capacity, $low, $high) = (30, 6,7, 840,900); #-- simulation control my $Improvements; # count how often we could improve (reduce error) the # current model state during the last watchdog interval my $maxloops = 250_000; # give up after that many tries my $watchdog = 50_000; # check current simulation state my $loops = 0; # current number of simulation iterations my $sum; $sum += $_ for @a; my $mean = $sum / @a; #-- find biggest partition of bins with max. capacity # assumption: only two partitions with bins of max- and min-capacity my $j_max = find_biggest_partition(); print "partitions: ${j_max}x${max_capacity} ", $bin_no - $j_max, "x$min_capacity\n"; #-- balance partitions -> mean capacity: my $mean_cap = ($max_capacity * $j_max + $min_capacity * ($bin_no - $j_max)) / $bin_no; printf "mean-cap. : %.3f\n", $mean_cap; my $target_sum = int( $mean * $mean_cap + 0.99 ); die "reduce low ($low) below $target_sum" if $low > $target_sum; print "sum : $sum\n"; print "target-sum: $target_sum\n"; print "mean : $mean\n"; #-- create bins my @bins = map { [ 0, [] , $target_sum, $low, $high ] } (0..$bin_no-1); # bin-element-legend: # [0]: sum of [1] # [1]: [ array of items ] # [2]: individual target / used for error-computation # [3]: individual low -limit for [0] # [4]: individual high-limit for [0] # - note: individual fields [2..4] might be tweaked - currently it is overkill # shuffle input - allows another try if a simulation fails @a = shuffle @a; #-- PHASE 1 - satisfy first constraint (bin-"geometry") # first, fill biggest partition with $max_capacity items my $idx = 0; foreach my $i (0..$j_max-1) { push @{$bins[$i]->[1]}, @a[ $idx .. $idx + $max_capacity - 1 ]; $bins[$i]->[0] += $_ for (@{$bins[$i]->[1]}); # sum up $idx += $max_capacity; # advance pointer } # fill rest (if any) with $min_capacity items foreach my $i ($j_max..$bin_no-1) { push @{$bins[$i]->[1]}, @a[ $idx .. $idx + $min_capacity - 1 ]; $bins[$i]->[0] += $_ for (@{$bins[$i]->[1]}); # sum up $idx += $min_capacity; # advance pointer # $bins[$i]->[2] = $indi_sum; # we could assign an indiv. target sum # $bins[$i]->[3] = $indi_lo; # we could assign an indiv. low-limit # $bins[$i]->[4] = $indi_hi; # we could assign an indiv. high-limit } die "assertion: failed to partition \@a" if $idx != @a; print "inital err.: ", error_ind(), "\n"; # PHASE 2 - try to optimise bins by swapping elements while (1) { my @wrongs = find_invalid(); last unless @wrongs; # ready! # simulation-control $loops++; unless ($loops % $watchdog) { print_result(); die "no solution - tune and re-start\n" if ($Improvements == 0 or $loops >= $maxloops); $Improvements = 0; # here, one could start a little mutation and see if that helps - feel free to experiment # for (1..5) { # my ($i,$j) = (pick_a_bin(\@bins), pick_a_bin(\@bins)); # swap_bin_element($bins[$j], $bins[$i]); # } } #-- ideal: take it from the rich and give it to the poor ;-) # (aka Robin Hood distribution) my @poor = grep { $_->[0] < $_->[3] } @bins; # bins where sum is too low my @rich = grep { $_->[0] > $_->[4] } @bins; # bins where sum is too high if (!@rich || !@poor) { # ok: a list of elements that fulfill size requirements my @ok = grep { $_->[0] >= $_->[3] and $_->[0]<=$_->[4]} @bins; @rich = @ok unless @rich; @poor = @ok unless @poor; } # fall-back to any (can this happen?) @rich = @bins unless @rich; @poor = @bins unless @poor; # finally try a swap that might lead to an improvement my ($i,$j) = (pick_a_bin(\@poor), pick_a_bin(\@rich)); swap_bin_element($rich[$j], $poor[$i]); } print "=== SOLUTION FOUND ===\n"; print print_result(); exit; sub find_invalid { # check: bins out of [low <= sum <= high] range my @inv_bins = grep { $_->[0] < $_->[3] || $_->[0]>$_->[4] } @bins; return @inv_bins if @inv_bins; # check: bins out of capacity-range @inv_bins = grep { @{$_->[1]} < $min_capacity || @{$_->[1]}>$max_capacity } @bins; return @inv_bins; } sub pick { # pick a random element from a given bin-ref return int rand( @{$_[0]->[1]} ); } sub pick_a_bin { # pick a random element from an array return int rand( @{$_[0]} ); } #-- compute some error measure / indication # (strictly, this is not really a variation-coefficient) sub error_ind { my ($deltasum2, $d); for (@bins) { $d = ($_->[0] - $_->[2]); # deviation from target $deltasum2 += ($d * $d); # deviation squared } return $deltasum2 / ( $mean * $#bins ); } #-- swap two bin elements if the swap yields to an improvement sub swap_bin_element { my ($b1, $b2) = @_; return if $b1 == $b2; my ($i1, $i2) = (pick($b1), pick($b2)); # random strategy my $last_err = error_ind(); # (todo: candidate for caching) my $val1 = $b1->[1]->[$i1]; my $val2 = $b2->[1]->[$i2]; $b1->[0] -= $val1; $b2->[0] -= $val2; $b1->[0] += $val2; $b2->[0] += $val1; if (error_ind() < $last_err) { # improvement --> move (*) $Improvements++; # we're getting better ($val1) = splice @{$b1->[1]}, $i1 , 1; ($val2) = splice @{$b2->[1]}, $i2 , 1; push @{$b1->[1]}, $val2; push @{$b2->[1]}, $val1; } else { # undo $b1->[0] -= $val2; $b2->[0] -= $val1; $b1->[0] += $val1; $b2->[0] += $val2; } # (*) - nice location to introduce some mutations # (e.g. ... or rand(1000)<$mutation_threshold ...) } sub find_biggest_partition { ## generate / check partitioning / assume exactly two partitions my $j_max = -1; # max no. of bins with max. capacity for my $i (0..int( @a/$min_capacity )) { for my $j (0..int(@a/$max_capacity)) { $j_max = $j if ( $i * $min_capacity + $j * $max_capacity == @a # covers @a and $i + $j == $bin_no # sums to bins and $j > $j_max); # best solution } } die "cannot find a partition using min-/max-capacites " . "$min_capacity/$max_capacity\n" if $j_max < 0; return $j_max; } sub print_result { my $runtime = time() - $^T; my $bin; print "runtime : ${runtime}s, loops:$loops, improv.:$Improvements\n"; print "ranges : low: $low, target: $target_sum, high: $high\n"; print "capacity: min: $min_capacity, meancap: $mean_cap, max: $max_capacity\n"; print "error. : ", error_ind()," bins: $bin_no\n"; for (@bins) { my $sum = $_->[0]; my $err = ( $sum >= $low and $sum <= $high) ? "OK" : "ERR"; printf("%2d) %4d %-4s (sz=%d, trgt=%6.1f, [%3d-%3d]) %s\n", ++$bin, $sum, $err, 0+@{$_->[1]}, $_->[2], $_->[3], $_->[4], join(", ", map { sprintf("%4d", $_) } sort { $a <=> $b } @{$_->[1]} ) ); } } # Update: spelling/grammar/comments