holli has asked for the wisdom of the Perl Monks concerning the following question:

Given the program below, I need a function (here "find_lances") that finds and returns all groups of mechs that have exactly n members and weigh up to m tons, while every mech can only occur once per group ("lance").
use strict; use warnings; my %mechs; while (<DATA>) { chomp; my @d = split /\s+/; $mechs{$d[0]} = { weight => $d[1], fraction => $d[2] }; } # find all lances with 5 members and 380t weight my @possible_lances = find_lances (3, 150); sub find_lances { # big ? here } __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
Given the example above (3 members, 150 t) the result should be:
[ ["Flea", "Commando", "Hunchback"], ["Flea", "Commando", "BlackHawk"], ... ]
but not
[ ["Flea", "Flea", "Hunchback"], ]
(no Mech is allowed twice in a lance.)

and not
[ ["Flea", "Flea", "Atlas"], ]
(to heavy)


holli, /regexed monk/

Replies are listed 'Best First'.
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.

      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.


      holli, /regexed monk/
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->() ) { # ... }

    - tye        

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.
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

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
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 ;-)


    holli, /regexed monk/

      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

        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.


        holli, /regexed monk/