Re: [SPOILER] Solution to Perl 'Expert' Quiz-of-the-Week #22

Colin Meyer <[email protected]>
Newsgroups gmane.comp.lang.perl.qotw.discuss
Message-ID <[email protected]>
On Sun, Aug 29, 2004 at 12:08:31PM -0400, Daniel Martin wrote:
> Colin Meyer <[email protected]> writes:
> 
> Is there some reason you used this:
> 
> >    sub _rungs {
> >       my $w = shift;
> >       return map { my $c = $w; substr( $c, $_, 1 ) = ''; $c } 0..($SIZE-1);
> >    }
> 
> Instead of this?
> 
> sub _rungs {
>    my $w = shift;
>    return map { my $c = $w; substr( $c, $_, 1 ) = '_'; $c } 0..($SIZE-1);
> }
> 
> Then, you wouldn't have to keep track of what position each rung came
> from, and your %rung2word hash could have a much simpler structure
> (since it could just be $rung2word{$rung} = [$word1, $word2, ...];).
> 
> This also simplifies the code of morph, which is now just a map and a
> grep: [...]

I tried out Daniel's suggestions, and also simplified my searching
data structure some. The speed increase was fairly minimal, about 8%,
which isn't really noticeable when the longest searches are two seconds.

The benefit is that it makes the code a tad easier on the eyes.

-Colin.


#!/usr/local/bin/perl -l
# Colin Meyer [email protected] 2004-08-26
# with Daniel Martin's suggestions

# true for on, false for off
my $CASE_SENSITIVE = 0;
my $DEBUG = 0;

# get stuff from command line
my $start = shift; $start = lc $start unless $CASE_SENSITIVE;
my $end   = shift; $end   = lc $end   unless $CASE_SENSITIVE;
my $DICT = shift;
$DICT ||= '/usr/share/dict/words';

my $size = length( $start );
die "different lengths: $start -> $end.\n" 
   unless $size == length( $end );

build_db( $size );

for ( $start, $end ) {
   die "$_ isn't in the dictionary" unless in_db( $_ ) 
}   

# take $start word. find all words that it morphs to. take $end word,
# do same. Any words in start list equal to end list? if so, we've found
# a solution (check for other solutions at this stage, report shortest).
# Otherwise reiterate, for each word in the lists. track words that have
# been visited, to avoid circles. if ever can't find any more words,
# then give up.

my ( $starts,     # possible paths from the starting word
     $ends,       # possible paths from the ending word
                  # see expand_paths for description of hashref structure
     @solutions,  
     $visited,    # tracks where we've been, so that we don't wander in circles 
     @joins       # track which word joined the start list to the end list
                  # just for curiosity (displayed when $DEBUG is true)
   );

# seed the data search structures
$starts->{ $start }{ list } = [ $start ];
$ends  ->{ $end   }{ list } = [ $end   ];

my $notdone = 1;
while ( $notdone ) {
   my $sc = expand_paths( $starts );
   my $ec = expand_paths( $ends   );

   die "no path!\n" unless ( $sc || $ec );

   # check to see if we've found the solution
   for my $leading_word ( keys %{ $starts } ) {
      if ( exists $ends->{ $leading_word } ) {
         my @list;

         # the half of the solution from $start
         push @list, @{ $starts->{ $leading_word }{ list } };

         # remove the duplicate, matching word
         push @joins, pop @{ $ends->{ $leading_word }{ list } }; 

         # the half from $end
         push @list, reverse @{ $ends->{ $leading_word }{ list } };

         push @solutions, \@list;
         $notdone = 0;
      }
   }
}

my $solution = shift @solutions;
for ( @solutions ) {
   $solution = $_ if @$solution > @$_
} 

print for @$solution;
print scalar( @$solution ), " steps";

END {
  if ( $DEBUG ) {
    require 'YAML.pm'; 
    print "### Joining Words: @joins";
    print YAML::Dump( \@solutions );
    print "### STARTS";
    print YAML::Dump( $starts );
    print "### ENDS";
    print YAML::Dump( $ends );
  }
}


# passed in a hashref, whose keys are the words on the "leading edge" of
# the path, and whose values are hasherefs with these contents:
#   beenseen - a marker so that each list is only expanded once
#   list     - an arrayref of the path that lead up to this word
#
# action is to take each path and create several new paths, by adding all
# of the words that its current leading words can morph to (that haven't
# been used in any of the existing paths). these new paths are added 
# directly back into the hash.
#
# returns a count of how many new paths were created
#
sub expand_paths {
   my $h = shift;
   my $c = 0;
   WORD:
   for my $word ( keys %{ $h } ) {
      next WORD if $h->{ $word }{ beenseen }++;
      for my $nextword ( grep ! $h->{ $_ }, morph( $word ) ) {
         $c++;
         $h->{ $nextword }{ list } = 
               [ @{ $h->{ $word }{ list } }, $nextword ];
      }
   }
   return $c;
}


# database consists of two hashes, %word2rung and %rung2word each word
# has l rungs, where l is its length. a rung is the word with '_'
# substituted for one of its letters.
{
   my ( %word2rung, %rung2word, @words, $SIZE );

   sub build_db {
      $SIZE = shift;

      open DICT, '<', $DICT or die "can't open $DICT: $!";
      @words = grep length( $_ ) == $SIZE, 
               map { chomp; $CASE_SENSITIVE ? $_ : lc $_ } <DICT>;
      close DICT;

      for my $w ( @words ) {
         my @rungs = _rungs( $w );
         $word2rung{ $w } = \@rungs;
         push @{ $rung2word{ $_ } }, $w for @rungs;
      }
   }
   
   sub _rungs {
      my $w = shift;
      return map { my $c = $w; substr( $c, $_, 1 ) = '_'; $c } 0..($SIZE-1);
   }
   
   sub in_db {
      return grep( /$_[0]/, @words );
   }
   
   sub morph {
      my $w = shift;
      my @steps; 
      push @steps, map @$_, @rung2word{ @{ $word2rung{ $w } } };
      return grep $_ ne $w, @steps;
   }
}
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.