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

Dear Perl experts,

I'm way too tired now to try this and would like to ask you for help. Not to implement - I'll try tomorrow again - but preferably some pointers. I'm sure this has been done before:

I need a subroutine that is given an arbitrary number of lists and returns the cartesian product of these lists - preserving the ordering. e.g.

# comb([qw(a b c)],[qw(1 2 3)],[qw(- + *)]); # -> [[a,1,-],[a,1,+],[a,1,*], # [a,2,-],[a,2,+],[a,2,*], # [a,3,-],[a,3,+],[a,3,*], # [b,1,-],[b,1,+],[b,1,*], # [b,2,-],[b,2,+],[b,2,*], # [b,3,-],[b,3,+],[b,3,*], # [c,1,-],[c,1,+],[c,1,*], # [c,2,-],[c,2,+],[c,2,*], # [c,3,-],[c,3,+],[c,3,*]] #

You get the idea. The problem for me is, that the number of lists given is variable and I simply don't get the required iteration/recursion to achieve that combinatorics.

Many thanks for any pointers.

Punymonk

Replies are listed 'Best First'.
Re: cartesian product preserving order
by Zaxo (Archbishop) on Jun 08, 2005 at 22:42 UTC

    glob can do that,

    my $combs = [ map { [split //] } glob '{a,b,c}{1,2,3}{-,+,\*}' ];

    After Compline,
    Zaxo

      Wow! Just wow! However, this solution seems not to cope with strings of arbitrary length. So I did some cargo cult programming and "invented" the delimiter.
      my $combs = [ map { [split /\./] } glob '{CZK,EUR}.{asia,emea,na,sa}.{acad,sopr,user}' ];
      But it feels like I brought some kludginess to the code that way. Punymonk

        That's exactly how I would have handled longer elements. You can pick any seperator that suits the data.

        That's not cargo cult, it's adaptation. Glad you like it :-)

        After Compline,
        Zaxo

      Too bad the other usage of glob (out of file name expansion) aren't documented in the standard documentation...

        It's documented indirectly. perldoc -f glob says that glob expansion works as in csh. Try this in csh or bash:

        $ echo {a,b,c}{1,2,3} a1 a2 a3 b1 b2 b3 c1 c2 c3 $

        After Compline,
        Zaxo

Re: cartesian product preserving order
by ikegami (Patriarch) on Jun 08, 2005 at 22:22 UTC
    Algorithm::Loops's NestedLoops seems ideal for this task.
    use Algorithm::Loops qw( NestedLoops ); sub comb { my @rv; NestedLoops(\@_, sub { push(@rv, [ @_ ]); }); return \@rv; }

    Test harness:

    Update: What follows is a solution that doesn't require an external module. Neither it nor NestedLoops are recursive. Since they're not recursive, they're probably faster, more memory efficient, and you can use a callback instead of building the result in memory. You could even convert the code below into an iterator.

    sub comb { my @idx = map { 0 } @_; my @max = map { $#$_ } @_; my @rv; for (;;) { push(@rv, [ map { $_[$_][$idx[$_]] } 0..$#idx ]); my $i = 0; for (;;) { $idx[$i]++; last if $idx[$i] <= $max[$i]; $idx[$i] = 0; $i++; return \@rv if $i == @idx; } } }
Re: cartesian product preserving order
by brian_d_foy (Abbot) on Jun 09, 2005 at 00:06 UTC
Re: cartesian product preserving order
by BrowserUk (Patriarch) on Jun 09, 2005 at 00:51 UTC

    The recursive solution.

    sub comb{ return @{ $_[0] } if @_ == 1; map{ my $x = $_; map{ "$x$_" } comb( @_ ) } @{ shift() } } my @combs = comb [qw(a b c)],[qw(1 2 3)],[qw(- + *)];

    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    Lingua non convalesco, consenesco et abolesco. -- Rule 1 has a caveat! -- Who broke the cabal?
    "Science is about questioning the status quo. Questioning authority".
    The "good enough" maybe good enough for the now, and perfection maybe unobtainable, but that should not preclude us from striving for perfection, when time, circumstance or desire allow.
Re: cartesian product preserving order
by shemp (Deacon) on Jun 08, 2005 at 22:33 UTC
    One approach is to do the cartesian product of the first 2 lists, which it sounds like you already have working. Then use that partial cartesian product as 1 list, and take the cartesian product of that list against the next list, and so on, until you are out of lists.

    We aware that with this approach taking the product using one of the partial products will be slightly different than with 2 of the original lists. I.E.

    A partial list will look like:
    [a,1],[a,2],[a,3],[b,1],[b,2],[b,3]

    Whereas an original list will be like:
    (-, +, *)

    So when using a partial product be careful not to end up with something like:
    [[a,1],-]

    Hope this helps

Re: cartesian product preserving order
by Limbic~Region (Chancellor) on Jun 08, 2005 at 22:33 UTC
Re: cartesian product preserving order
by TedPride (Priest) on Jun 09, 2005 at 04:52 UTC
    glob is perhaps the prettiest solution, but it isn't recommended for large sets, since all combinations will be generated before any are returned. A recursive solution is better - something like the following:
    sub comb { my $s = shift; if (@_ == 1) { print "$s$_\n" for @{$_[0]}; return; } comb("$s$_",@_) for @{shift()}; } comb '',[qw(a b c)],[qw(1 2 3)],[qw(- + *)];
    Where you can change this line to do whatever you want with the combination:
    if (@_ == 1) { dowhatever("$s$_") for @{$_[0]}; return; }
    BrowserUK's solution is somewhat neater, but it's more a pull down approach, where all combinations are returned at once, rather than a push up approach, where only one combination is worked on at a time. You might as well use glob if you're going that route.
      glob is perhaps the prettiest solution, but it isn't recommended for large sets, since all combinations will be generated before any are returned.

      So does the comb you wrote.

      TedPride,
      ...but it isn't recommended for large sets, since all combinations will be generated before any are returned.

      What makes you think that? In scalar context, glob becomes an iterator. That isn't how it was used, but it could have been. How is a recursive solution any better - it builds the entire solution in memory before returning it.

      Cheers - L~R