So today I found a large pile of cards laying about my coffee table. After spotting the two empty card boxes on the floor nearby, I realized that picking up all of the cards would then require organizing them into their respective decks before replacing them into their boxes. My immediate thought was, "Man, I sure wish I could whip up a quick Perl script to do this for me!", a comment which surprised me, since it was the first of its kind. I guess I can be pretty lazy, but it was the first time my first thought was to turn to Perl for a physical-world problem.

Later on I began to think that perhaps there might exist a similar problem in cyberspace. A hypothetical "pile" of data items might require organizing into distinct groups that contain only one of each item. Not sorted within each group per say, just organized. The script that follows is my solution. I attempted to generalize the algorithm to organize piles of any item. I used the card-pile pickup problem only as a test case.

use strict; use List::Util qw(shuffle); # construct a standard deck my @card; foreach my $suit (qw(H C S D)) { foreach my $value (2..10, qw(J Q K A)) { push @card, $value . $suit; } } # now shuffle us up a pile of cards, perhaps composed of any # number of decks my @pile; my $n = 4; # number of decks in shuffled pile push @pile, @card for (1..$n); @pile = shuffle @pile; # i think i'll try to generalize the algorithm such that it organizes # the pile into distinct groups that each have only one of each item. # in this test case, it will organize the pile into the appropriate # number of decks, each containing only one of each card. my $group = [[]]; while (my $item = shift @pile) { # search the current groups and add if item not found my $found; foreach my $i (0..$#$group) { unless ( grep { $item eq $_ } @{$group->[$i]} ) { push @{$group->[$i]}, $item; $found = 1; last; } } # if not found, make a new group with that item in it push @$group, [$item] unless $found; } # now print the results to see our handiwork foreach (0..$#$group) { print join(" ", @{$group->[$_]}), "\n\n" }

Prints the following (will differ for each shuffle):

10S QH 9S 6D 4S 10H JD JS KS AD 9C 7C KH 6S 8S 4D 10D 5D 10C QC 7S 9D +7H QS KD 2D 6H JC 5H 3D 8H JH 2S 9H 5C AC 2H AS 3C 8C KC 4C 5S 7D 3H +QD 8D 3S 2C AH 4H 6C 9C JS KH KS 4D QH 10S 7S AD 4S QS QC 5D 5H 8S 6H JD KD 9S 10D 5C 10H 8 +H 10C AS JH 2D 7D 6S 3H KC 3C 9H AC 9D 3D 4C 2H QD AH 8C 2C 3S JC 2S +7H 4H 6D 7C 6C 8D 5S 10S 4S QS 6H 8S 7S KS 10C 7D KD 4D AD JH 9C 9S QH KH 3D 5D 9D 6S 3H QD + 5H 2H JC 8C 2D 2S JD 3C 3S 10H AH AS 6D 2C QC 8H 4C 7C 9H 10D 4H JS +AC KC 5C 8D 6C 5S 7H QD 4S 7S JC JH AD 3H 8S QH 3D QS KS 4D 10C KD 9D 6H 3S QC 6S 6D 8C 10S + 8H 7C AS 2D 2S AH 7D JD 5C 9C KC 8D 9H 10H KH 3C 2H 4H 4C 5D JS 10D +2C 5S 9S AC 5H 7H 6C

Now that the problem's solved, I wonder if there really is a need for this algorithm. But, I guess that's what makes it a meditation.

Replies are listed 'Best First'.
Re: 52 Perl Pickup
by ikegami (Patriarch) on Aug 02, 2007 at 04:30 UTC

    The inner loop can be replaced with a hash to change the algorithm from O(N2) to O(N), a major improvement.

    my @groups; my %in_num_groups; while (@pile) { my $item = shift @pile; push @{$groups[$in_num_groups{$item}++]}, $item; }

    I also fixed the needless use of a reference to the array of groups and the inability to store false values in @pile.

    Update: For fun, did you know @pile could also be initialized using

    @pile = shuffle( (@card) x $n );
      ikegami, your new solution is flawless. Indeed, I had intended to ask for improvements upon my method at the end of my note, but apparently forgot to. Your hash method improves the exact portion of my algorithm that I felt the least confident about. I hate using flags like $found ... too ugly I guess. I knew there surely was a better way, but couldn't quite wrap my mind around it. Thank you very much, as I will be sure to remember this method for future problems.
Re: 52 Perl Pickup
by bobf (Monsignor) on Aug 02, 2007 at 04:43 UTC

    If you're going to play 52 Perl Pickup, you might as well use CPAN. :-)

    use strict; use warnings; use Games::Cards; my $num_decks = 2; # Create a deck of cards my $game = Games::Cards::Game->new( {} ); # default game my $deck = Games::Cards::Deck->new( $game, 'Deck' ); $deck->shuffle; # Assemble the pile of cards my @pile; push( @pile, map { $_->print() } @{ $deck->cards } ) for 1 .. $num_dec +ks; my @sorted_pile = sort @pile; # Compute the number of decks in the pile (pretend we don't know it) my $num_decks_calc = scalar @sorted_pile / $deck->size; # Divide the pile into decks my @decks; while( @sorted_pile ) { foreach my $decknum ( 0 .. $num_decks_calc-1 ) { push( @{ $decks[$decknum] }, pop( @sorted_pile ) ); } } # Print the results foreach my $decknum ( 0 .. $num_decks_calc-1 ) { print join( ' ', @{ $decks[$decknum] } ), "\n\n"; }

    Note: error checking could be added (missing cards, etc).

Re: 52 Perl Pickup
by misc (Friar) on Aug 02, 2007 at 19:47 UTC
    I've written a solution, too. :-)

    I haven't been able to determine the complexity, I'm too bad in mathematics, maybe someone else could ?

    However, the lower bound clearly tops all other algorithms, its g(1).
    Ok, the upper bound could be g(unlimited), I'd guess.

    use strict; use List::Util qw(shuffle); # construct a standard deck my @card; foreach my $suit (qw(H C S D)) { foreach my $value (2..10, qw(J Q K A)) { push @card, $value . $suit; } } # now shuffle us up a pile of cards, perhaps composed of any # number of decks my @pile; my $n = 4; # number of decks in shuffled pile push @pile, @card for (1..$n); @pile = shuffle @pile; my $decks; do { $decks = grab ( \@pile ); # split the pile into $n decks @pile = lookup( $decks ); # Look if we got it right.. } while ( @pile > 0 ); print "Got it!\n"; for my $a (0..$n){ print "$_ " foreach ( @{$decks->{$a}} ); print "\n\n"; } # Exits here.. happily sub grab{ my $pile = shift; my $decks; # Grab the cards foreach my $card (@{$pile}){ push @{$decks->{int(rand($n))}}, $card; } return $decks; } sub lookup{ my $decks = shift; for my $a (0..$n-1) { my %cards; foreach my $card ( @{$decks->{$a}} ){ if ( defined( $cards{$card} ) ){ damnit( $decks ); return throw ( $decks ); } else { $cards{$card} = 1; # Remember this. An +d dont forget it! } } } } sub damnit{ my $decks = shift; print "Damnit!!!\n"; for my $a (0..$n){ print "$_ " foreach ( @{$decks->{$a}} ); print "\n\n"; } } sub throw{ my $decks = shift; my @pile; foreach ( values %{$decks} ){ push @pile, @{$_}; } return shuffle( @pile ); }
Re: 52 Perl Pickup
by wind (Priest) on Aug 04, 2007 at 01:15 UTC
    Your algorithm does not accurately represent how one would achieve grouping of decks in real life. The easiest way to do that would almost certainly be to sort. First by suit, creating 4 individual piles. And the sorting each suit pile by rank. And finally separating the larger sorted pile of cards into individual decks.

    The following script represents this algorithmically:
    use List::Util qw(shuffle); use strict; # Ordered least to greatest my @suit = qw(C D H S); my @rank = ( 2 .. 10, qw(J Q K A) ); # Construct a standard deck my @card = map { my $suit = $_; map { "$_$suit" } @rank } @suit; # Shuffle 4 piles of cards. my @pile = shuffle( (@card) x 4 ); print "Pile\n\t@pile\n\n"; # Sort my %ord_suit = map { $suit[$_] => $_ } ( 0 .. $#suit ); my %ord_rank = map { $rank[$_] => $_ } ( 0 .. $#rank ); my @pile_sorted = map { $_->[0] } sort { $ord_suit{ $a->[1][1] } <=> $ord_suit{ $b->[1][1] } || $ord_rank{ $a->[1][0] } <=> $ord_rank{ $b->[1][0] } } map { [ $_, [m/(.*)(.)/] ] } @pile; # Group Cards into Decks my @deck; my $lastcard = ''; my $index; for my $card (@pile_sorted) { if ( $card ne $lastcard ) { $index = 0; } else { $index++; } push @{ $deck[$index] }, $card; $lastcard = $card; } print "Decks\n"; foreach (@deck) { print "\t@$_\n"; }
    - Miller
      And if you want to do this more generically:

      Define an unique order for each object. In this instance that happens to just be the card's value. Then create a list of hashes where a card is only added if one of the same order does not exist.
      use List::Util qw(shuffle first); use strict; # Ordered least to greatest my @suit = qw(C D H S); my @rank = (2..10, qw(J Q K A)); # Construct a standard deck my @card = map { my $suit = $_; map {"$_$suit"} @rank } @suit; # Shuffle 4 piles of cards. my @pile = shuffle( (@card) x 4 ); print "Pile\n\t@pile\n\n"; # Group Cards into Decks my @deck; for my $card (@pile) { # Order of Object my $ord = $card; my $deck = first {! exists $_->{$ord}} @deck; # Start new deck if (! $deck) { $deck = {}; push @deck, $deck; } $deck->{$ord} = $card; } # Results: print "Decks:\n"; for my $deck (@deck) { print "\t" . join(' ', values %$deck) . "\n"; }
      When you view the results, you'll notice that each of the "decks" appear to be near identical. This is because of the internal implementation of hashes and the way that elements are assigned a place in the hash.

      - Miller
      My previous post was a non-sequitur, sorry. It would be easier if the dealer wasn't colour-blind. We could factor in the backs.
      #!/usr/bin/perl -w # Re-sorting a pile of four decks. Unfazed by missing cards. # Using tr to avoid the unfathomable card-sort map. # Mock names for easy sorting my @suit = qw(f g h i); my @rank = (2..9, qw(a b c d e)); my @pile= (); sub shuffle { for (my $i = $#_ ; $i > 0 ; $i--) { @_[$_, $i] = @_[$i, $_] for rand $i+1 } return @_; } sub readable { my $string = join(" ", @_); $string =~ tr/abcdefghi/TJQKACDHS/; return $string; } sub separate { my @cards = sort @_; my @box = (); my $deck = 0; my $prevcard = ""; for my $card (@cards) { if ($card ne $prevcard) {$deck = 0}; push @{$box[$deck++]}, $card; $prevcard = $card; } return @box; } for (1..4) { push (@pile, (map {$rank = $_; map {"$_$rank"} @suit} @rank)); } shuffle @pile; print "\nFour decks shuffled together\n"; print (readable @pile); my @decks = separate @pile; print "\n\nFour decks separated\n"; foreach (@decks) { print (readable @$_); print "\n"; }
        Which can be whittled down to a more useful PNB dealer which would normally be written to a file. I did it to feed the the GIB command-line bridge engine.
        #!/usr/bin/perl ## Dealing cards in PBN (Portable Bridge Notation) ## Using tr to avoid the awesome card-sort map ## mock names for easy sorting my @suit = qw(f g h i); my @rank = (2..9, qw(a b c d e)); my @seps = qw(g. h. i.); sub shuffle { for (my $i = $#_ ; $i > 0 ; $i--) { @_[$_, $i] = @_[$i, $_] for rand $i+1 } return @_; } ## reformats one hand at a time sub pbnFormat { push (@_, @seps) ; local $string = join(``, sort{$b cmp $a}@_); $string =~ tr/abcdefghi/TJQKA/d; return $string; } my @deck = shuffle(map{$rank=$_; map{"$_$rank"}@suit}@rank); for(1..4) { print pbnFormat splice @deck,0,13; print "\n"; }