Re: find combinations
by Molt (Chaplain) on Sep 06, 2005 at 10:45 UTC
|
Okay, the simplest approach here is to get all the combinations (via a convenient CPAN module.. Math::Combinatorics) and then filter those for the valid lances. There are almost certainly faster and more efficient ways to do this, but this seems to at least work.
| [reply] [d/l] |
|
|
I have discovered that module while you were writing the node and came up with the following code (which is similar to yours).
But thanks for your effort.
| [reply] [d/l] |
Re: find combinations (NestedLoops)
by tye (Sage) on Sep 06, 2005 at 14:18 UTC
|
Quick, untested Algorithm::Loops hack, but it doesn't require a huge list of nested arrays to be stored before you sort through them looking for the ones to keep:
use Algorithm::Loops qw( NestedLoops );
sub GenLances
{
my( $hvMechs, $members, $maxWeight )= @_;
my @mechs= keys %$hvMechs;
my $iter= NestedLoops(
[ [0..$#mechs], (sub{[$_+1..$#mechs]})x($members-1) ],
{
OnlyWhen => sub {
return if @_ != $members;
$weight= 0;
$weight += $_->{weight} for @$hvMechs{@mechs[@_]};
return $weight <= $maxWeight;
},
}
);
return sub {
my @idx= $iter->();
return @mechs[ @idx ];
};
}
# ...
my $iter= GenLances( \%mechs, 3, 150 );
my @lance;
while( @lance= $iter->() ) {
# ...
}
| [reply] [d/l] |
Re: find combinations
by Roy Johnson (Monsignor) on Sep 06, 2005 at 17:28 UTC
|
Just for fun/whatever insight it might bring, here's a recursive, no-modules solution that generates only the matching combinations.
use warnings;
use strict;
# A mech is an arrayref of three attributes: name, weight, 'clan'
my @mechs = map [split], <DATA>;
# find all lances with M members and <=W weight
my @possible_lances = find_lances (3, 150, @mechs);
print 'Found '.@possible_lances, " sets\n";
print join(',', map($_->[0], @$_)), "\n" for @possible_lances;
# Return an array of arrayrefs. Each arrayref contains a set (lance) o
+f mechs.
sub find_lances
{
my ($M, $W, $first, @rest) = @_;
# If there aren't enough mechs left, or you're overweight, no matc
+h
if ($M < 1 or $M-1 > @rest or $W < 0) { return () }
# Base case
elsif ($M == 1) {
return(map [$_], grep { $_->[1] <= $W } $first, @rest);
}
else {
return (
# All qualifying sets that include the 1st element
do {
if ($first->[1] <= $W) {
map [$first, @$_],
find_lances($M-1, $W-$first->[1], @rest);
}
else { () }
},
# and all qualifying sets that do not include the 1st
+element
find_lances($M, $W, @rest)
);
}
}
__DATA__
Flea 20 clan
Commando 25 clan
UrbanMech 30 clan
Hollander 35 clan
Jenner 35 clan
Raven 35 clan
Wolfhound 35 clan
BlackHawk 50 clan
Hunchback 50 clan
Rifleman 60 clan
Catapult 65 clan
Loki 65 clan
Thor 70 clan
MadCat 75 clan
Gargoyle 80 clan
Victor 80 clan
Zeus 80 clan
Longbow 85 clan
Warhawk 85 clan
Mauler 90 clan
Atlas 100 clan
Caution: Contents may have been coded under pressure.
| [reply] [d/l] |
Re: find combinations
by QM (Parson) on Sep 06, 2005 at 15:26 UTC
|
I've just been reading MJD's Higher Order Perl, which gives the development for the similar "sums to exactly X" problem (but I don't have the book with me to quote directly).
Alternatively, there's Algorithm::Knapsack, but it claims it's brute force, and doesn't handle complex data structures.
Perhaps following the lead of HOP wrt callbacks, you can convert A::K to something more generally useful?
Update: HOP gives this solution for the exact partition problem (p 206):
# Usage: @list = partition( $some_target, \@list );
sub partition {
my ($target, $treasures) = @_;
return [] if $target == 0;
return () if $target < 0 || @$treasures == 0;
my ($first, @rest) = @$treasures;
my @solutions = partition($target-$first, \@rest);
return ((map{[$first,@$_]} @solutions),
partition($target, \@rest));
}
MJD goes on to improve on this in various ways, but that should suit you for now :)
(Unfortunately, the HOP examples website isn't up yet, so I had to type this in by hand, and it's untested.)
-QM
--
Quantum Mechanics: The dreams stuff is made of
| [reply] [d/l] |
Re: find combinations (choose)
by demerphq (Chancellor) on Sep 07, 2005 at 08:03 UTC
|
Well, here's my go. Since I wasn't familiar too with Algorithm::Loops when i started (ive since looked into it) I wrote my own iterator generator. Actually it was the part of this problem I found interesting. Especially as it seems there are tons of modules for producing permutations but none that do choose in a simple way. I guess Algorithm::Loops is the closest, but the notationational requirements seem somewhat clumsy to me. IMO a shortcut for this task would be nice.
After post update: Heh, and now i learn of Math::Combinatorics. Well it was a nice exercise anyway. :-)
use strict;
use warnings FATAL => 'all';
use Data::Dumper;
# make_choose_iter( $choose, @list )
# ------------------------------------------------------
# Returns an anonymous subroutine to act as an iterator
# over the set of combinations, returning each in turn
# every time it is called. If the iter is called with an
# argument or the list has been exhausted on a previous run
# it resets itself and returns undef.
#
sub make_choose_iter {
my ( $choose, @list )= @_;
die "Can't do ".(0+@list)." choose ".($choose||0)."\n"
if $choose>@list or !@list or !$choose;
$choose--; # 1 based number passed in, but need it 0 based
# done is used as a flag, and @idx is the first n positions
my ( $done, @idx )= ( 0, 0 .. $choose );
return sub {
if ( $done || @_ ) {
# if we finished the list last call, or they are
# forcing a reset via an argument
# then we reset everthing and return undef/empty list
($done , @idx)= ( 0, 0 .. $choose );
return;
}
# we get the apropriate elements of the list for the
# current state of @idx. this is what we will return
my @ret= @list[@idx];
# assume that this is the last element.
$done= 1;
# Loop over the elements of @idx from the right
# looking for the correct element:
# We are simulating the behaviour of N
# nested loops. This means we increment the
# rightmost element whose increment does not
# overflow and whose final value is sufficiently
# low that the elements following it can be filled
# with its successors without also overflowing
for ( my $i= $choose ; $i >= 0 ; $i-- ) {
if ( $idx[$i] < $#list
&& (my $end= $idx[$i] + 1 + $choose - $i) < @list )
{
$idx[$i]++;
@idx[ $i + 1 .. $choose ]= ( $idx[$i] + 1 .. $end );
$done= 0;
last;
}
}
return wantarray ? @ret : \@ret;
};
}
sub find_lances {
my ($mechs,$count,$weight)=@_;
my $iter= make_choose_iter( $count,
sort { $b->{weight} <=> $a->{weight} } values %$mechs
);
my @ret;
while (my $items=$iter->()) {
my $sum=0;
$sum+=$_->{weight} for @$items;
if ( $sum <= $weight ) {
push @$items,$sum;
push @ret,$items;
}
}
return @ret;
}
my %mechs;
while (<DATA>) {
chomp;
my ($mech,$weight,$clan) = split /\s+/,$_,3;
next unless $mech;
$mechs{$mech} = {
name => $mech,
weight => $weight,
group => $clan
};
}
# find all lances with 5 members and 380t weight
my @possible_lances = find_lances (\%mechs, 5, 380);
print Dumper(\%mechs,\@possible_lances);
__DATA__
Flea 20 clan
Commando 25 clan
UrbanMech 30 clan
Hollander 35 clan
Jenner 35 clan
Raven 35 clan
Wolfhound 35 clan
Black 50 Hawk clan
Hunchback 50 clan
Rifleman 60 clan
Catapult 65 clan
Loki 65 clan
Thor 70 clan
Mad 75 Cat clan
Gargoyle 80 clan
Victor 80 clan
Zeus 80 clan
Longbow 85 clan
Warhawk 85 clan
Mauler 90 clan
Atlas 100 clan
| [reply] [d/l] |
Re: find combinations
by holli (Abbot) on Sep 07, 2005 at 10:45 UTC
|
I hacked a benchmark together that compares tye's, demerphq's and my version:
And this is the result:
Benchmark: timing 10 iterations of demerphq, holli, tye...
demerphq: 7 wallclock secs ( 6.28 usr + 0.00 sys = 6.28 CPU) @ 1
+.59/s (n=10)
holli: 30 wallclock secs (29.53 usr + 0.03 sys = 29.56 CPU) @ 0
+.34/s (n=10)
tye: 0 wallclock secs ( 0.00 usr + 0.00 sys = 0.00 CPU)
(warning: too few iterations for a reliable count)
output identical:!
tye's version is the fastest, demerphq's is a bit slower and I am at the last position with my brute force attempt. All solutions produce the same output, just the sort order is differtent. Now guess what I will use ;-)
| [reply] [d/l] [select] |
|
|
Unfortunately the benchmark is misleading. You are timing how long it takes tye's solution to return an iterator that can give you the full set, not the amount of time it takes that iterator to return the full set like you are for your solution and my solution. Anyway, heres a modified benchmark that compares apples to apples. (Although I admit I tweaked my code slightly to not pass so much on the stack.)
Also, I normalized and/or simplified some of the other code involved, and changed the way the %mechs has gets passed into your code. Bottom subbing has the disadvantage that you get accidental globals, which can cause trouble when you convert things to a module. Its good practice to top-sub for this reason.
And it shows my code beating tyes by just a tiny bit. Repeated attempts show the difference is pretty consistant so I dont think this is noise.
Benchmark: running demq, holli, tye for at least 10 CPU seconds...
demq: 17 wallclock secs (10.31 usr + 0.02 sys = 10.33 CPU) @ 2
+.71/s (n=28)
holli: 19 wallclock secs (11.73 usr + 0.00 sys = 11.73 CPU) @ 0
+.34/s (n=4)
tye: 15 wallclock secs (10.14 usr + 0.03 sys = 10.17 CPU) @ 2
+.06/s (n=21)
Rate holli tye demq
holli 0.335/s -- -85% -87%
tye 2.18/s 550% -- -18%
demq 2.66/s 695% 22% --
output identical
Anyway, let this be a lesson to you. When the disparity of the benchamrks is so high look for a mistake in the code being benchmarked, especially when you get warnings about insufficient iterations. (And its generally better to use time for the benchmark and not iterations.)
---
$world=~s/war/peace/g
| [reply] [d/l] [select] |
|
|
I stand corrected. I changed my benchmark code to
timethese
(-10,
{
'tye' => sub
{
$iter = find_lances_tye ( \%mechs, 5, 240, 10 );
while ( my $lance = $iter->() )
{
push @tye, join (",", sort @{$lance->{names}}) . "(" .
+ $lance->{weight} . " tons)\n" ;
}
@tye = sort @tye;
},
'holli' => sub
{
@holli = find_lances_holli (5, 240, 10, keys %mechs );
@holli = map { join (",", sort @{$_->{names}}) . "(" . $_-
+>{weight} . " tons)\n" } @holli;
@holli = sort @holli;
},
'demerphq' => sub
{
@demerphq = find_lances_demerphq ( \%mechs, 5, 240, 10 );
@demerphq = map { join (",", sort @{$_->{names}}) . "(" .
+$_->{weight} . " tons)\n" } @demerphq;
@demerphq = sort @demerphq;
},
}
);
so it constructs the same data-structure in all cases and then I get
C:\>benchmark.pl
Benchmark: running demerphq, holli, tye for at least 10 CPU seconds...
demerphq: 10 wallclock secs (10.41 usr + 0.02 sys = 10.42 CPU) @ 1
+.44/s (n=15)
holli: 12 wallclock secs (12.23 usr + 0.00 sys = 12.23 CPU) @ 0
+.33/s (n=4)
tye: 10 wallclock secs (10.33 usr + 0.02 sys = 10.34 CPU) @ 1
+.84/s (n=19)
Pretty similar to your outcome. Thanks for the clarification.
| [reply] [d/l] [select] |
|
|