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

What I need is a sort of permutation, but limited, and given sets. For example, say I have:

@one = qw{C A T}; @two = qw{D O G}; @three = qw{B I R D};

I want to be shown as results:

CDB ADB TDB COB COI etc...

So, I always want an element of the first set first, the second second, and third last. I don't want:

DAR BOT etc...

I have been trying to hack some permutation code to not give me evert combination possible, and to only give me combinations in the order I want, as well as accepting sets, but so far to no avail. Anyone know of something which does this, or have a good solution? The available permutation modules can't seem to do this, or if they can it isn't obvious to me. Even some decent pseudo-code would help me on this one.. thanks.

Cheers,
KM

Replies are listed 'Best First'.
Re: Sort of a permutation
by lhoward (Vicar) on Oct 18, 2000 at 21:15 UTC
    You could use a recursive permute function:
    sub permute{ my ($prefix,$c,@arrays)=@_; my @ret=(); foreach(@$c){ my $f=$prefix.$_; if(scalar(@arrays)==0){ push @ret,$f; }else{ push @ret,@{permute($f,@arrays)}; } } return \@ret; } my $perms=permute('',['C','A','T'],['D','O','G'],['B','I','R',D']);
    this is a bit slower than the standard nested-loop approach, but it has the advantage that it is generic and you don't need to alter your code if you have more lists. (btw, I seem to remember merlyn having a similar, but much slicker permute function that does more or less the same thing lying around somewhere)
Re: Sort of a permutation
by Blue (Hermit) on Oct 18, 2000 at 20:14 UTC
    If you're looking at just generating all permutations you could just nest loops to go through each array. Off the top of my head, it would look like:

    foreach $l1 (@one) { foreach $l2 (@two) { foreach $l3 (@three) { $permutation = $l1 . $l2 . $l3; # Do whatever } } }
    This is very ugly code - take it more like the pseudo-code you asked for. I'm not a Perl adept, and I normally hesitate to put code where some of the giants of Perl here may see it, but what the heck.

    Does this help?

    =Blue
    ...you might be eaten by a grue...

      Hmmm.. not really the way I would want to do it if at all possible. But, I could try to make that way work. I think I may have been overthinking this :)

      Cheers,
      KM

Re: Sort of a permutation
by chromatic (Archbishop) on Oct 18, 2000 at 23:04 UTC
    Here's a more general sort of solution. It's not particularly clean, but it works:
    #!/usr/bin/perl -w use strict; my @one = qw{C A T}; my @two = qw{D O G}; my @three = qw{B I R D}; my @combos = permute(\@one, \@two, \@three); show_text(\@combos, \@one, \@two, \@three); sub permute { my @arrays = @_; my @lengths; foreach my $array_ref (@arrays) { push @lengths, scalar @$array_ref; } return combine(@lengths); } sub combine { my $length = shift; my @results; for (0 .. ($length - 1)) { if (@_) { foreach my $result (combine(@_)) { push @results, $_ . $result; } } else { push @results, $_; } } return @results; } sub show_text { my ($combos, @arrays) = @_; foreach my $combo (@$combos) { my $i = 0; my $text = ''; foreach my $elem (split'', $combo) { $text .= $arrays[$i++]->[$elem]; } print "$text\n"; } }
      I took the way chromatic was doing it, and changes little bits and modularized it. At some point, I may actually make this more useful and subclass it for phone number formats other than here in the US. Anyways, this is a quick hack.

      I wanted to do this becuase, as some of you know, I recently moved. This means I need to remember a host of new phone numbers, which I am not particularly good at. So, instead of memorizing my new home number, work number, the number of family, friends and cow-orkers, it is useful to (try to) make words out of phone numbers. For example, my old phone number was: xxx-7342. The 7342 spells SEGA, which was easy for me and others to remember. I wanted to do this again.

      The following module will accept a 7 digit phone number, and find all the possible letter combinations for those numbers. Then, it uses Text::Ispell to see what combinations are words in my dictionary. It can try the first 3 numbers, last 4 numbers, and all 7. Again, this is a quick hack, but it was fun, and I can now easily remember my new home number :) Perl to the rescue!

      package PhoneToWord; use Exporter; @ISA = qw( Exporter ); @EXPORT = qw( ); use strict; use Text::Ispell qw(spellcheck); sub new { my ($class,$num) = @_; $class = ref($class) || $class; my $self = {}; bless $self, $class; my @numbers = ([qw(0)], # No letters for 0 [qw(1)], # ditto [qw(A B C)], [qw(D E F)], [qw(G H I)], [qw(J K L)], [qw(M N O)], [qw(P Q R S)], [qw(T U V)], [qw(W X Y Z)] ); $num =~ s/[- ]//g; my @nums = split //, $num; $self->{THREE} = [$numbers[$nums[0]], $numbers[$nums[1]], $numbers[$nums[2]]]; $self->{FOUR} = [$numbers[$nums[3]], $numbers[$nums[4]], $numbers[$nums[5]], $numbers[ +$nums[6]]]; $self->{SEVEN} = [$self->{THREE}, $self->{FOUR}]; return $self; } sub first_three { my $self = shift; my @combos = $self->permute(@{$self->{THREE}}); my @retval = $self->get_words(\@combos, @{$self->{THREE}}); return wantarray ? @retval : \@retval; } sub last_four { my $self = shift; my @combos = $self->permute(@{$self->{FOUR}}); my @retval = $self->get_words(\@combos, @{$self->{FOUR}}); return wantarray ? @retval : \@retval; } sub seven { my $self = shift; my @combos = $self->permute(@{$self->{SEVEN}}); my @retval = $self->get_words(\@combos, @{$self->{SEVEN}}); return wantarray ? @retval : \@retval; } sub get_words { my $self = shift; my $combos = shift; my @uses = @_; my @ret; my @words = join " ", $self->show_text($combos, @uses); # spellcheck was spewing out crap I didn't want so # the eval shuts it up. eval { for my $word (spellcheck(@words)) { if ($word->{type} =~ /(?:ok|compound)/) { push @ret, $word->{term}; } } }; return @ret; } sub permute { my $self = shift; my @arrays = @_; my @lengths; for my $array_ref (@arrays) { push @lengths, scalar @$array_ref; } return $self->combine(@lengths); } sub combine { my $self = shift; my $length = shift; my @results; for (0 .. ($length - 1)) { if (@_) { foreach my $result ($self->combine(@_)) { push @results, $_ . $result; } } else { push @results, $_; } } return @results; } sub show_text { my ($self, $combos, @arrays) = @_; my @all; foreach my $combo (@$combos) { my $i = 0; my $text; for my $elem (split'', $combo) { $text .= $arrays[$i++]->[$elem]; } push @all, $text; } return @all; } 1;

      And a quick example:

      #!/usr/bin/perl -w use strict; use PhoneToWord; my $foo = new PhoneToWord qw(663-7375); my @three = $foo->first_three(); my $four = $foo->last_four(); my @all = $foo->seven(); print join "\n", @three; print "\n"; print join "\n", @$four; print "\n"; print join "\n", @all; # Output (with my dictionary, of course) MOD MOE NOD ONE PERL So, I had no seven letter words, 4 three letter ones, and one 4 letter + one. So, this phone number (not mine, so don't call it :) can be ONE-PERL.

      Remember, this is a quick hack.. if I decide to make it more intuitive, I will repost at a later date.

      Cheers,
      KM

(tye)Re: Sort of a permutation
by tye (Sage) on Oct 18, 2000 at 22:51 UTC

    I see no reason to resort to recursion:

    #!/usr/bin/perl -w use strict; my @letter= map { [ split // ] } qw( CAT DOG BIRD ); my @idx= (0) x @letter; while( 1 ) { print join( "", map { $letter[$_][$idx[$_]] } 0..$#letter ), "\n"; my $i= 0; while( $i < @letter && $#{$letter[$i]} < ++$idx[$i] ) { $idx[$i++]= 0; } last if @letter <= $i; }
            - tye (but my friends call me "Tye")
RE: Sort of a permutation
by little (Curate) on Oct 18, 2000 at 20:49 UTC
    Seems to me that you could also use a Matrix, where the letters are represented by their chr_code, missing fields simply filled with zeros, than you have quite comfortable access to the data stored as well. But it's just a thaught I couldn't stop from crossing through my head :-))
    Have a nice day
    All decision is left to your taste
RE: Sort of a permutation
by lachoy (Parson) on Oct 18, 2000 at 22:30 UTC