Beefy Boxes and Bandwidth Generously Provided by pair Networks
Just another Perl shrine
 
PerlMonks  

Printing combinations, code review request

by SparkeyG (Curate)
on Apr 26, 2003 at 12:25 UTC ( [id://253348]=perlquestion: print w/replies, xml ) Need Help??

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

I was looking for code to print all the combinations of a set taken two at a time. Looking here, I found code to print all the permutations of a list. I took that, bent it to my will, and came up with what's below.

It works as I want, I am just wondering if this could be done better or more efficently.
--SparkeyG

#!/usr/bin/perl my @list = (1 .. 5); my $take = 2; comb(\@list, $take, []); sub comb { my @items = @{ $_[0] }; my $group = $_[1]; my @list = @{ $_[2] }; unless ($group) { print "@list\n"; } else { my (@newitems,@newlist,$i); foreach $i (0 .. $#items) { @newlist = @list; push (@newlist, shift (@items)); @newitems = @items; comb([@newitems], $group - 1, [@newlist]); } } }

Replies are listed 'Best First'.
Re: Printing combinations, code review request
by demerphq (Chancellor) on Apr 26, 2003 at 13:06 UTC

    I came up with the below. Basically my intention was to avoid copying the arrays unless necessary.

    #!/usr/bin/perl use strict; use warnings; my @list = (1 .. 5); my $take = 2; comb_demerphq(\@list, $take, []); sub comb_demerphq { my ($items,$group,$list)=@_; return _comb_demerphq([ @{ $items||[] } ],$ group, [ @{ $list||[] +} ] ); } sub _comb_demerphq { my ($items,$group,$list) = @_; unless ($group) { print "@$list\n"; } else { my @newlist = (@$list,undef); while (@$items) { $newlist[-1]=shift (@$items); _comb_demerphq([@$items], $group - 1, \@newlist); } } }

    But i suspect that there are still better ways to improve this by using a different algorithm. Not sure what it is though.


    ---
    demerphq

    <Elian> And I do take a kind of perverse pleasure in having an OO assembly language...
Re: Printing combinations, code review request
by integral (Hermit) on Apr 26, 2003 at 13:27 UTC

    Like demerphq I wanted to reduce the ammount of array copying in your code, but I also didn't like the way the input array had to be copied, so I came up with the following (although you may like to change my coding style in places):

    #!/usr/bin/perl use strict; use warnings; my @list = (1 .. 5); my $take = 2; comb_integral(\@list, $take); sub comb_integral { my ($items, $group, $list, $next) = @_; $list ||= []; $next ||= 0; if ($group == 1) { my $prefix = join " ", @$list; print "$prefix $_\n" for @$items[$next..$#$items]; } else { for my $i ($next..$#$items) { push @$list, $$items[$i]; # the next line is an alternate which elimates the use of push/pop #comb_integral($items, $group - 1, [@$list, $$items[$i]], $i + 1); comb_integral($items, $group - 1, $list, $i + 1); pop @$list; } } }

    --
    integral, resident of freenode's #perl
    

      Nice!

      I tried something like this but couldn't quite get my head around it. I found the iterative approach a lot easier to grok in the end. I added your code to my benchmark., but I couldn't get the non push/pop version going. If you tell me what change I need to do (it should return an AoA instead of printing the list of elements) then I'll add that variant in as well.

      ++ to you for sure. :-)


      ---
      demerphq

      <Elian> And I do take a kind of perverse pleasure in having an OO assembly language...

      • Update:  
      Once I looked at the code again I realized my error and added both versions to the benchmark. The push/pop variant is faster. Nice stuff.


        That comment points out a version of the code which instead of using the same array to hold all the elements which we're currently 'on', creates a separate array each time. To use it change the body of the for loop from:

        push @$list, $$items[$i]; comb_integral($items, $group - 1, $list, $i + 1); pop @$list;

        to the single line:

        comb_integral($items, $group - 1, [@$list, $$items[$i]], $i + 1);

        And here's the new-improved version which returns the combinations instead of printing them, although it does create and destroy a few anonymous arrays in the process:

        #!/usr/bin/perl use strict; use warnings; use Data::Dumper; my @list = (1 .. 5); my $take = 2; my @combs = comb_integral(\@list, $take); for my $comb (@combs) { print "@$comb\n"; } sub comb_integral { my ($items, $group, $next) = @_; $next ||= 0; if ($group == 1) { return map [$_], @$items[$next..$#$items]; } else { my @returns; for my $i ($next..$#$items) { push @returns, map [$$items[$i], @$_], comb_integral($items, $gr +oup - 1, $i + 1); } return @returns; } }

        In fact I've just thought of another variant of that, replace the push line in the for loop with:

        push @returns, my @combs = comb_integral($items, $group - 1, $i + 1); unshift @$_, $$items[$i] for @combs;

        Although some might prefer not to code quite so tersely ;-).

        --
        integral, resident of freenode's #perl
        

        Update: I've just benchmarked this (as integral2) and don't bother with the variant suggested at the end as it's the slowest of my four versions. This does beat by two earlier ones however. Here's my benchmark results:

Re: Printing combinations, code review request
by demerphq (Chancellor) on Apr 26, 2003 at 14:50 UTC

    Hmm, I was right about changing the algorithm, this routine doesnt need recursion, and it doesnt need to be throwing arrays around everywhere. I get a significant speed up by using the following code

    readmore is the full benchmark!
    sub comb_iter { my $items = shift || []; # Think 1,2,3,4,5 my $group = shift || 2; # Think 2 @$items>=$group or die "Insufficient elements ".scalar(@$items). " to make groups of $group\n"; my @index=(0..$group-1); # Think 0,1 my @last=(@$items-$group..$#$items); # Think 3,4 my @ret; while ($index[0]<=$last[0]) { # Think first pass: 0 < 3 push @ret,[@$items[@index]]; # increment the last digit, and if we rollover carry it left unless (++$index[-1]<=$last[-1]) { # we've rolled over, we need to go left until # until the result after adding 1 is below the # respective last. my $pos=$#index; --$pos while ($pos and $index[$pos]+1>$last[$pos]); # increment the apropriate index, if we increment # $index[0] too high the loop exits because of the # while condition. my $v=++$index[$pos]; # and then move right assigning the values in sequential o +rder $index[++$pos]=++$v while $pos<$#index; } } return \@ret; }

    A good point of attack to make an algorithm more efficient is to look at eliminating recursion where you can. People have a tendency to use the stack and recursion to take care of their messy business. This is ok in some respects like ease of design, or speed of implementation, but often has the side effect of being slow, as usually more ends up being moved around and done than needs to be. Whenever possible its good to look for the iterative solution, and only fall back to a recursive one if the iterative eludes you.

    <edited tofit="your screen, for your viewing pleasure"> Benchmark: running Sparky, demphq, int_i2, int_ni, int_np, int_pp, ite +rat, each for at least 1 CPU seconds... Sparky: 1 w-secs ( 1.07 usr + 0.00 sys = 1.07 CPU) @ 8358.21/s +(n=8960) demphq: 1 w-secs ( 1.12 usr + 0.00 sys = 1.12 CPU) @ 11914.36/s + (n=13356) int_i2: 1 w-secs ( 1.04 usr + 0.00 sys = 1.04 CPU) @ 15889.53/s + (n=16541) int_ni: 1 w-secs ( 1.05 usr + 0.00 sys = 1.05 CPU) @ 18582.70/s + (n=19549) int_np: 1 w-secs ( 1.06 usr + 0.00 sys = 1.06 CPU) @ 18407.72/s + (n=19549) int_pp: 1 w-secs ( 1.04 usr + 0.00 sys = 1.04 CPU) @ 18761.04/s + (n=19549) iterat: 1 w-secs ( 1.06 usr + 0.00 sys = 1.06 CPU) @ 22394.91/s + (n=23761) Rate Sparky demphq int_i2 int_np int_ni int_pp iterat Sparky 8358/s -- -30% -47% -55% -55% -55% -63% demphq 11914/s 43% -- -25% -35% -36% -36% -47% int_i2 15890/s 90% 33% -- -14% -14% -15% -29% int_np 18408/s 120% 55% 16% -- -1% -2% -18% int_ni 18583/s 122% 56% 17% 1% -- -1% -17% int_pp 18761/s 124% 57% 18% 2% 1% -- -16% iterat 22395/s 168% 88% 41% 22% 21% 19% -- </edited>

    Anyway, that was fun, thanks.


    ---
    demerphq

    <Elian> And I do take a kind of perverse pleasure in having an OO assembly language...

    • Update:  
    Added integrals code to the BM. I couldnt get the non push/pop version working correctly, probably my fault. Also I should mention that originally I altered all of the solutions to return an AoA of the results, instead of printing them out. This was needed to make the benchmark possible.
    • Update:  
    Ok, now I feel stupid. I got both version of integrals code to work now. It was a dumb mistake that I noticed after I updated. I also reorganized to the BM code to make it easier to update the node here.
    • Update:  
    Added integrals two new variants. Renamed tests to make graph fit


Re: Printing combinations, code review request
by terrencebrown (Acolyte) on Apr 27, 2003 at 19:26 UTC
    How is the result needed? Is it to be stored in and array for future use or just printed?
      And, is the array sequential numbers from 1 .. 10, or 15 .. 25, or non-sequential like 1,2,5,7,8, or text elements like names?
        The results were going to end up in an array and the input array needs only be an array of n ellements. I choose 1..5 as an example set.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://253348]
Approved by Corion
Front-paged by astaines
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others browsing the Monastery: (3)
As of 2024-04-20 01:35 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found