File Coverage

blib/lib/AI/Pathfinding/SMAstar/Examples/Phrase.pm
Criterion Covered Total %
statement 280 505 55.4
branch 98 216 45.3
condition 22 66 33.3
subroutine 18 41 43.9
pod 0 29 0.0
total 418 857 48.7


line stmt bran cond sub pod time code
1             #
2             #
3             # Author: matthias beebe
4             # Date : June 2008
5             #
6             #
7              
8             package AI::Pathfinding::SMAstar::Examples::Phrase;
9 1     1   3227 use Tree::AVL;
  1         3  
  1         109  
10 1     1   7 use AI::Pathfinding::SMAstar::Examples::PalUtils;
  1         2  
  1         33  
11 1     1   6 use strict;
  1         3  
  1         61  
12              
13             BEGIN {
14 1     1   5 use Exporter ();
  1         2  
  1         82  
15 1     1   18 @AI::Pathfinding::SMAstar::Examples::Phrase::ISA = qw(Exporter);
16 1         3 @AI::Pathfinding::SMAstar::Examples::Phrase::EXPORT = qw();
17 1         24 @AI::Pathfinding::SMAstar::Examples::Phrase::EXPORT_OK = qw($d);
18              
19             }
20              
21 1     1   5 use vars qw($d $max_forgotten_nodes); # used to debug destroy method for accounting purposes
  1         2  
  1         18851  
22             $d = 0;
23             $max_forgotten_nodes = 0;
24              
25              
26             ##################################################
27             ## the Phrase constructor
28             ##################################################
29             sub new {
30 9     9 0 27 my $invocant = shift;
31 9   33     30 my $class = ref($invocant) || $invocant;
32 9         126 my $self = {
33             _word_list => undef,
34             _words_w_cands_list => undef,
35             _dictionary => undef,
36             _dictionary_rev => undef,
37             _start_word => undef, # remainder on cand for antecedent of this obj
38             _word => undef,
39             _cand => undef, # cand found for the antecedent of this obj
40             _predecessor => undef,
41             _dir => 0,
42             _repeated_pal_hash_ref => {},
43             _match_remainder_left => undef,
44             _match_remainder_right => undef,
45             _letters_seen => undef, # letters seen, up to/including antecedent
46             _cost => undef, # cost used for heuristic search
47             _cost_so_far => undef,
48             _num_chars_so_far => undef, # cummulative cost used for heuristic
49             _num_new_chars => undef,
50             _no_match_remainder => undef, # flag specifying whether there was a remainder
51             _phrase => undef,
52             _depth => 0,
53             _f_cost => undef,
54             @_, # Override previous attributes
55             };
56              
57 9         55 return bless $self, $class;
58            
59             }
60              
61             ##############################################
62             ## methods to access per-object data
63             ##
64             ## With args, they set the value. Without
65             ## any, they only retrieve it/them.
66             ##############################################
67              
68             sub start_word {
69 0     0 0 0 my $self = shift;
70 0 0       0 if (@_) { $self->{_start_word} = shift }
  0         0  
71 0         0 return $self->{_start_word};
72             }
73              
74             sub word {
75 0     0 0 0 my $self = shift;
76 0 0       0 if (@_) { $self->{_word} = shift }
  0         0  
77 0         0 return $self->{_word};
78             }
79              
80             sub cand {
81 0     0 0 0 my $self = shift;
82 0 0       0 if (@_) { $self->{_cand} = shift }
  0         0  
83 0         0 return $self->{_cand};
84             }
85              
86             sub antecedent{
87 0     0 0 0 my $self = shift;
88 0 0       0 if (@_) { $self->{_predecessor} = shift }
  0         0  
89 0         0 return $self->{_predecessor};
90             }
91              
92              
93              
94             sub dir{
95 0     0 0 0 my $self = shift;
96 0 0       0 if (@_) { $self->{_dir} = shift }
  0         0  
97 0         0 return $self->{_dir};
98             }
99              
100             sub match_remainder_left{
101 0     0 0 0 my $self = shift;
102 0 0       0 if (@_) { $self->{_match_remainder_left} = shift }
  0         0  
103 0         0 return $self->{_match_remainder_left};
104             }
105              
106             sub match_remainder_right {
107 0     0 0 0 my $self = shift;
108 0 0       0 if (@_) { $self->{_match_remainder_right} = shift }
  0         0  
109 0         0 return $self->{_match_remainder_right};
110             }
111              
112             sub intersect_threshold {
113 0     0 0 0 my $self = shift;
114 0 0       0 if (@_) { $self->{_intersect_threshold} = shift }
  0         0  
115 0         0 return $self->{_intersect_threshold};
116             }
117              
118             sub max_collisions{
119 0     0 0 0 my $self = shift;
120 0 0       0 if (@_) { $self->{_max_collisions} = shift }
  0         0  
121 0         0 return $self->{_max_collisions};
122             }
123              
124             sub letters_seen{
125 0     0 0 0 my $self = shift;
126 0 0       0 if (@_) { $self->{_letters_seen} = shift }
  0         0  
127 0         0 return $self->{_letters_seen};
128             }
129              
130             sub f_cost{
131 0     0 0 0 my $self = shift;
132 0 0       0 if (@_) { $self->{_f_cost} = shift }
  0         0  
133 0         0 return $self->{_f_cost};
134             }
135              
136             sub depth{
137 0     0 0 0 my $self = shift;
138 0 0       0 if (@_) { $self->{_depth} = shift }
  0         0  
139 0         0 return $self->{_depth};
140             }
141              
142             sub is_completed{
143 0     0 0 0 my $self = shift;
144 0 0       0 if (@_) { $self->{_is_completed} = shift }
  0         0  
145 0         0 return $self->{_is_completed};
146             }
147              
148             sub is_on_queue{
149 0     0 0 0 my $self = shift;
150 0 0       0 if (@_) { $self->{_is_on_queue} = shift }
  0         0  
151 0         0 return $self->{_is_on_queue};
152             }
153              
154             sub descendants_deleted{
155 0     0 0 0 my $self = shift;
156 0 0       0 if (@_) { $self->{_descendants_deleted} = shift }
  0         0  
157 0         0 return $self->{_descendants_deleted};
158             }
159              
160             sub need_fval_change{
161 0     0 0 0 my $self = shift;
162 0 0       0 if (@_) { $self->{_need_fcost_change} = shift }
  0         0  
163 0         0 return $self->{_need_fcost_change};
164             }
165              
166              
167              
168            
169              
170             sub compare
171             {
172 0     0 0 0 my ($min_letters) = @_;
173              
174             return sub{
175 0     0   0 my ($self, $arg_obj) = @_;
176              
177 0         0 my $self_eval_func = evaluate($min_letters);
178 0         0 my $argobj_eval_func = evaluate($min_letters);
179 0         0 my $self_eval = $self->$self_eval_func;
180 0         0 my $arg_obj_eval = $arg_obj->$argobj_eval_func;
181            
182 0         0 return $self_eval - $arg_obj_eval;
183             }
184 0         0 }
185              
186              
187              
188             sub compare_by_depth
189             {
190 0     0 0 0 my ($self, $arg_obj) = @_;
191            
192 0         0 my $self_depth = $self->{_depth};
193 0         0 my $argobj_depth = $arg_obj->{_depth};
194            
195 0         0 my $result = $self_depth - $argobj_depth;
196            
197 0         0 return $result;
198             }
199              
200              
201              
202             # compare_phrase_word_strings
203             #
204             # usage: $phrase_obj->compare_phrase_word_strings($other_word_obj)
205             #
206             # Accepts another Phrase object as an argument.
207             # Returns 1 if greater than argument, 0 if equal, and -1 if
208             # less than argument
209             sub compare_phrase_word_strings
210             {
211 0     0 0 0 my ($self, $arg_obj) = @_;
212            
213 0         0 my $arg_phrase_plus_word = $arg_obj->{_phrase} . $arg_obj->{_word};
214 0         0 my $phrase_plus_word = $self->{_phrase} . $self->{_word};
215            
216 0 0       0 if($arg_phrase_plus_word gt $phrase_plus_word){
    0          
217 0         0 return -1;
218             }
219             elsif($arg_phrase_plus_word eq $phrase_plus_word){
220 0         0 return 0;
221             }
222 0         0 return 1;
223             }
224              
225              
226              
227             #----------------------------------------------------------------------------
228             # evaluation function f(n) = g(n) + h(n) where
229             #
230             # g(n) = cost of path through this node
231             # h(n) = distance from this node to goal (optimistic)
232             #
233             # used for A* search.
234             #
235             sub evaluate
236             {
237 1     1 0 455 my ($min_num_letters) = @_;
238             return sub{
239            
240 7     7   8 my ($self) = @_;
241              
242             # if fcost has already been calculated (or reassigned during a backup)
243             # then return it. otherwise calculate it
244 7         19 my $fcost = $self->{_f_cost};
245 7 50       12 if(defined($fcost)){
246 0         0 return $fcost;
247             }
248              
249 7         8 my $word = $self->{_start_word};
250 7         9 my $cost = $self->{_cost};
251 7         13 my $cost_so_far = $self->{_cost_so_far};
252 7         8 my $num_new_chars = $self->{_num_new_chars};
253 7         7 my $num_chars_so_far = $self->{_num_chars_so_far};
254              
255 7 100       15 my $phrase = defined($self->{_phrase}) ? $self->{_phrase} : "";
256 7         8 my $len_phrase = length($phrase);
257 7         46 my $phrase_num_chars = AI::Pathfinding::SMAstar::Examples::PalUtils::num_chars_in_word($phrase);
258            
259 7         9 my $ratio = 0;
260 7 100       15 if($phrase_num_chars){
261 5         7 $ratio = $len_phrase/$phrase_num_chars;
262             }
263              
264              
265             #my $total_cost = $cost_so_far + $cost;
266 7         10 my $total_cost = $cost_so_far + $cost + $ratio;
267             #my $total_cost = 0; # greedy search (best-first search)
268             #my $distance_from_goal = 0; # branch and bound search. optimistic/admissible.
269            
270 7         9 my $distance_from_goal = $min_num_letters - ($num_chars_so_far + $num_new_chars); #1 optimistic/admissible
271              
272 7         7 my $evaluation = $total_cost + $distance_from_goal;
273 7         10 $self->{_f_cost} = $evaluation;
274              
275 7         17 return $evaluation;
276             }
277 1         10 }
278              
279             #-----------------------------------------------------------------------------
280             sub phrase_is_palindrome_min_num_chars
281             {
282 1     1 0 2 my ($min_num_chars) = @_;
283            
284             return sub{
285 6     6   7 my ($self) = @_;
286            
287 6         8 my $phrase = $self->{_phrase};
288            
289 6 100 100     22 if(AI::Pathfinding::SMAstar::Examples::PalUtils::is_palindrome($phrase) &&
290             (AI::Pathfinding::SMAstar::Examples::PalUtils::num_chars_in_pal($phrase) >= $min_num_chars)){
291 1         4 return 1;
292             }
293             else{
294 5         11 return 0;
295             }
296             }
297 1         14 }
298              
299            
300            
301             #----------------------------------------------------------------------------
302             sub letters_seen_so_far
303             {
304 0     0 0 0 my ($self) = @_;
305 0         0 my $num_letters_seen = $self->{_num_chars_so_far};
306            
307 0         0 return $num_letters_seen;
308             }
309              
310              
311              
312              
313              
314              
315              
316              
317              
318              
319              
320              
321              
322              
323              
324              
325              
326              
327              
328              
329              
330              
331              
332              
333              
334              
335              
336             #-----------------------------------------------------------------------------
337             # Get descendants iterator function.
338             # Generate the next descendant of a phrase object. Each descendant adds
339             # another word to the phrase that could possibly lead to a palindrome
340             #
341             #-----------------------------------------------------------------------------
342             sub get_descendants_iterator
343             {
344 12     12 0 14 my ($phrase_obj) = @_;
345 12 50       23 if(!$phrase_obj){
346 0         0 return;
347             }
348            
349 12         13 my $words = $phrase_obj->{_word_list};
350 12         13 my $words_w_cands = $phrase_obj->{_words_w_cands_list};
351 12         14 my $dictionary = $phrase_obj->{_dictionary};
352 12         14 my $dictionary_rev = $phrase_obj->{_dictionary_rev};
353 12         19 my $repeated_pal_hash_ref = $phrase_obj->{_repeated_pal_hash_ref};
354 12         14 my $letters_seen = $phrase_obj->{_letters_seen};
355 12         14 my $cost = $phrase_obj->{_cost};
356 12         10 my $cost_so_far = $phrase_obj->{_cost_so_far};
357 12         13 my $num_chars_so_far = $phrase_obj->{_num_chars_so_far};
358 12         13 my $no_match_remainder = $phrase_obj->{_no_match_remainder};
359 12         24 my $depth = $phrase_obj->{_depth};
360 12         16 my $direction = $phrase_obj->{_dir};
361 12         18 my $word = $phrase_obj->{_start_word};
362 12         14 my $whole_word = $phrase_obj->{_cand};
363 12 100       18 my $len_whole_word = defined($whole_word) ? length($whole_word) : 0;
364 12         18 my $rev_word = reverse($word);
365 12         11 my $len_word = length($word);
366 12         14 my @cands;
367             my @descendants;
368              
369            
370 12 100       22 if($direction == 0){
    50          
371 8         21 @cands = AI::Pathfinding::SMAstar::Examples::PalUtils::get_cands_from_left($word, $dictionary, $dictionary_rev);
372             }
373             elsif($direction == 1){
374 4         9 @cands = AI::Pathfinding::SMAstar::Examples::PalUtils::get_cands_from_right($word, $dictionary, $dictionary_rev);
375             }
376            
377            
378            
379             #----------------Letters Seen-----------------------------------------------
380 12         29 my @sorted_letters_seen = sort(@$letters_seen);
381             # how much does this word collide with the letters seen so far, and what are the new letters?
382 12         30 my ($word_intersect, @differences) = AI::Pathfinding::SMAstar::Examples::PalUtils::word_collision($word, \@sorted_letters_seen);
383             # store the difference in new letters_seen value.
384 12         21 push(@sorted_letters_seen, @differences);
385            
386 12         13 my $new_num_chars_so_far = @sorted_letters_seen;
387             #-----------------------------------------------------------
388            
389              
390            
391              
392 12         11 my @words_to_make_phrases;
393             my $stored_c;
394              
395             return sub{
396            
397 9     9   11 LABEL1:
398             # this is a continuation of the second case below, where there were no
399             # match-remainders for the phrase-so-far, i.e. the palindrome has a space
400             # in the middle with mirrored phrases on each side. 'cat tac' for example.
401             my $next_word = shift(@words_to_make_phrases);
402 9 100       17 if($next_word){
403            
404 2         3 my $w = $next_word;
405              
406 2         2 my $repeated_word_p = 0;
407 2         4 my $antecedent = $phrase_obj->{_predecessor};
408 2         2 my $antecedent_dir = $antecedent->{_dir};
409 2         4 while($antecedent){
410              
411 2 0 33     5 if(defined($antecedent->{_word}) && $w eq $antecedent->{_word} && $antecedent_dir == 0){
      33        
412 0         0 $repeated_word_p = 1;
413 0         0 last;
414             }
415 2         2 $antecedent = $antecedent->{_predecessor};
416 2 50       6 if($antecedent){
417 0         0 $antecedent_dir = $antecedent->{_dir};
418             }
419             }
420              
421 2 50 33     15 if($repeated_word_p || $w eq $word){
422 0         0 goto LABEL1;
423             #next; #skip this word, we are already looking at it
424             }
425              
426             #----------------Compute the Cost-------------------------------------------
427 2         3 my $len_w = length($w);
428 2         7 my $collisions_per_length = AI::Pathfinding::SMAstar::Examples::PalUtils::collisions_per_length($w, $phrase_obj->{_phrase});
429 2         7 my $sparsity = AI::Pathfinding::SMAstar::Examples::PalUtils::get_word_sparsity_memo($w);
430 2         5 my $num_chars = AI::Pathfinding::SMAstar::Examples::PalUtils::num_chars_in_word_memo($w);
431 2         5 my ($word_intersect, @differences) = AI::Pathfinding::SMAstar::Examples::PalUtils::word_collision($w,
432             \@sorted_letters_seen);
433 2         3 my $num_new_chars = $num_chars - $word_intersect;
434             #my $newcost = $collisions_per_length + $sparsity;
435 2         2 my $newcost = $collisions_per_length + $len_w;
436 2         2 my $new_cost_so_far = $cost + $cost_so_far;
437              
438             #---------------------------------------------------------------------------
439 2         9 my $new_phrase = AI::Pathfinding::SMAstar::Examples::Phrase->new(
440             _word_list => $words,
441             #_words_w_cands_list => \@words_to_make_phrases,
442             _words_w_cands_list => $words_w_cands,
443             _dictionary => $dictionary,
444             _dictionary_rev => $dictionary_rev,
445             _start_word => $w,
446             _cand => $stored_c,
447             _word => $w,
448             _predecessor => $phrase_obj,
449             _dir => 0,
450             _repeated_pal_hash_ref => $repeated_pal_hash_ref,
451             _letters_seen => \@sorted_letters_seen,
452             _cost => $newcost,
453             _cost_so_far => $new_cost_so_far,
454             _num_chars_so_far => $new_num_chars_so_far,
455             _num_new_chars => $num_new_chars,
456             _no_match_remainder => 1,
457             _depth => $depth+1,
458             );
459            
460             #print "returning new phrase from first cond.\n";
461 2         8 $new_phrase->{_phrase} = $new_phrase->roll_up_phrase();
462 2         11 return $new_phrase;
463            
464             }
465             else{
466              
467 7         9 my $c = shift(@cands);
468 7 50       15 if(!$c){
469 0         0 return;
470             }
471            
472             # ------------- filter for repeated palcands for a particular word------
473             # ----------------------------------------------------------------------
474             # This will avoid many repeated patterns among palindromes to trim down the
475             # number redundant palindromes generated.
476             #
477 7         6 my $letters_seen_str = join("", @{$phrase_obj->{_letters_seen}});
  7         16  
478 7 100       13 if($letters_seen_str){
479 3         4 my $repeated_pal_hash_key;
480 3         5 $repeated_pal_hash_key = $word . "^" . $c . "^" . $letters_seen_str;
481            
482             #print "\n\nrepeated_pal_hash_key: $repeated_pal_hash_key\n";
483 3 50       7 if(my $hash_val = $repeated_pal_hash_ref->{$repeated_pal_hash_key}){
484             # skip because '$word' <--> '$p' pattern has already appeared in a previous palindrome.
485 3 50       8 if($hash_val != $depth){
486 0         0 goto LABEL1;
487             # next; # skip
488             }
489             }
490             else{
491             #flag this candidate as already having been tested (below).
492 0         0 $repeated_pal_hash_ref->{$repeated_pal_hash_key} = $depth;
493             }
494             }
495             #--------------------------------------------------------------------------
496             #--------------------------------------------------------------------------
497            
498 7         7 my $len_c = length($c);
499 7         9 my $rev_c = reverse($c);
500 7         8 my $word_remainder;
501            
502 7 100       22 if($len_c < $len_word){
    100          
503 2         2 $word_remainder = $c;
504             }
505             elsif($len_c > $len_word){
506 3         5 $word_remainder = $word;
507             }
508 7         10 my $rev_word_remainder = reverse($word);
509            
510 7         7 my $num_cands = @cands;
511            
512 7         64 my $match_remainder;
513             my $len_match_remainder;
514 0         0 my $newcost;
515 0         0 my $new_cost_so_far;
516 0         0 my $num_new_chars;
517 0         0 my $new_direction;
518            
519 7 100       17 if($direction == 0){
    50          
520 5 50       12 if($len_c < $len_word){
    100          
521 0         0 $match_remainder = AI::Pathfinding::SMAstar::Examples::PalUtils::match_remainder($word, $rev_c);
522 0         0 $new_direction = 0;
523             }
524             elsif($len_c > $len_word){
525 3         9 $match_remainder = AI::Pathfinding::SMAstar::Examples::PalUtils::match_remainder($rev_c, $word);
526 3         4 $match_remainder = reverse($match_remainder);
527 3         4 $new_direction = 1;
528             }
529             }
530             elsif($direction == 1){
531 2 50       12 if($len_c < $len_word){
    0          
532 2         5 $match_remainder = AI::Pathfinding::SMAstar::Examples::PalUtils::match_remainder($rev_word, $c);
533 2         3 $match_remainder = reverse($match_remainder);
534 2         2 $new_direction = 1;
535             }
536             elsif($len_c > $len_word){
537 0         0 $match_remainder = AI::Pathfinding::SMAstar::Examples::PalUtils::match_remainder($c, $rev_word);
538 0         0 $new_direction = 0;
539             }
540             }
541            
542 7 100       16 $len_match_remainder = defined($match_remainder) ? length($match_remainder) : 0;
543            
544             #----------------Compute the Cost-------------------------------------------
545 7 100       16 if($len_c < $len_word){
    100          
546 2         2 $num_new_chars = 0;
547 2         3 $newcost = 0; # new candidate is a (reversed) substring of word
548 2         3 $new_cost_so_far = $cost + $cost_so_far;
549             }
550             elsif($len_c > $len_word){
551            
552             #if($len_c != $len_word){
553 3         10 my $collisions_per_length = AI::Pathfinding::SMAstar::Examples::PalUtils::collisions_per_length($match_remainder, $phrase_obj->{_phrase});
554 3         8 my $num_chars = AI::Pathfinding::SMAstar::Examples::PalUtils::num_chars_in_word_memo($match_remainder);
555 3         7 my $sparsity = AI::Pathfinding::SMAstar::Examples::PalUtils::get_word_sparsity_memo($match_remainder);
556 3         10 my ($word_intersect, @differences) = AI::Pathfinding::SMAstar::Examples::PalUtils::word_collision_memo($match_remainder,
557             \@sorted_letters_seen);
558 3         6 $num_new_chars = $num_chars - $word_intersect;
559             #$newcost = $sparsity + $collisions_per_length;
560 3         5 $newcost = $collisions_per_length + $len_match_remainder;
561 3         4 $new_cost_so_far = $cost + $cost_so_far;
562             }
563             #---------------------------------------------------------------------------
564            
565 7 100       12 if($match_remainder){ # there is a length difference between the candidate and this word.
566 5         20 my $new_phrase = AI::Pathfinding::SMAstar::Examples::Phrase->new(
567             _word_list => $words,
568             _words_w_cands_list => $words_w_cands,
569             _dictionary => $dictionary,
570             _dictionary_rev => $dictionary_rev,
571             _start_word => $match_remainder,
572             _cand => $c,
573             _word => $whole_word,
574             _predecessor => $phrase_obj,
575             _dir => $new_direction,
576             _repeated_pal_hash_ref => $repeated_pal_hash_ref,
577             _letters_seen => \@sorted_letters_seen,
578             _cost => $newcost,
579             _cost_so_far => $new_cost_so_far,
580             _num_chars_so_far => $new_num_chars_so_far,
581             _num_new_chars => $num_new_chars,
582             _depth => $depth+1,
583             );
584             #print "returning new phrase from second cond.\n";
585 5         11 $new_phrase->{_phrase} = $new_phrase->roll_up_phrase();
586 5         19 return $new_phrase;
587             }
588             else{
589             #
590             # There is no match_remainder, so this candidate is the reverse
591             # of $word, or the phrase built so far is an "even" palindrome,
592             # i.e. it has a word boundary (space) in the middle.
593             #
594             #
595             # This is a special case since there is no match remainder.
596             # Because there is no remainder to create new phrase obj from, this
597             # section goes through the whole word list and creates phrase objects
598             # for each new word. The number of new characters offered by each
599             # word is recorded to help with guided search.
600             #
601             # Update: this case now only goes through the word list for which there
602             # are cands.
603            
604 2         5 @words_to_make_phrases = @$words_w_cands;
605             #@words_to_make_phrases = @$words;
606            
607            
608 2         2 $stored_c = $c;
609 2         4 my $next_word = shift(@words_to_make_phrases);
610 2         2 my $w = $next_word;
611            
612 2         2 my $repeated_word_p = 0;
613 2         4 my $antecedent = $phrase_obj->{_predecessor};
614 2         2 my $antecedent_dir = $antecedent->{_dir};
615 2         6 while($antecedent){
616            
617 2 0 33     7 if(defined($antecedent->{_word}) && $w eq $antecedent->{_word} && $antecedent_dir == 0){
      33        
618 0         0 $repeated_word_p = 1;
619 0         0 last;
620             }
621 2         2 $antecedent = $antecedent->{_predecessor};
622 2 50       7 if($antecedent){
623 0         0 $antecedent_dir = $antecedent->{_dir};
624             }
625             }
626            
627 2 50 33     12 if($repeated_word_p || $w eq $word){
628 2         82 goto LABEL1;
629             #next; #skip this word, we are already looking at it
630             }
631            
632             #----------------Compute the Cost-------------------------------------------
633 0         0 my $len_w = length($w);
634 0         0 my $collisions_per_length = AI::Pathfinding::SMAstar::Examples::PalUtils::collisions_per_length($w, $phrase_obj->{_phrase});
635 0         0 my $sparsity = AI::Pathfinding::SMAstar::Examples::PalUtils::get_word_sparsity_memo($w);
636 0         0 my $num_chars = AI::Pathfinding::SMAstar::Examples::PalUtils::num_chars_in_word_memo($w);
637 0         0 my ($word_intersect, @differences) = AI::Pathfinding::SMAstar::Examples::PalUtils::word_collision_memo($w,
638             \@sorted_letters_seen);
639 0         0 my $num_new_chars = $num_chars - $word_intersect;
640             #my $newcost = $collisions_per_length + $sparsity;
641 0         0 my $newcost = $collisions_per_length + $len_w;
642 0         0 my $new_cost_so_far = $cost + $cost_so_far;
643            
644             #---------------------------------------------------------------------------
645 0         0 my $new_phrase = AI::Pathfinding::SMAstar::Examples::Phrase->new(
646             _word_list => $words,
647             _words_w_cands_list => $words_w_cands,
648             _dictionary => $dictionary,
649             _dictionary_rev => $dictionary_rev,
650             _start_word => $w,
651             _cand => $c,
652             _word => $w,
653             _predecessor => $phrase_obj,
654            
655             _dir => 0,
656             _repeated_pal_hash_ref => $repeated_pal_hash_ref,
657             _letters_seen => \@sorted_letters_seen,
658             _cost => $newcost,
659             _cost_so_far => $new_cost_so_far,
660             _num_chars_so_far => $new_num_chars_so_far,
661             _num_new_chars => $num_new_chars,
662             _no_match_remainder => 1,
663             _depth => $depth+1,
664             );
665            
666             #print "returning new phrase from third cond.\n";
667 0         0 $new_phrase->{_phrase} = $new_phrase->roll_up_phrase();
668 0         0 return $new_phrase;
669            
670             }
671             }
672             }
673 12         119 }
674              
675              
676              
677              
678             #-----------------------------------------------------------------------------
679             # Return the number of successors of this phrase
680             #-----------------------------------------------------------------------------
681             sub get_num_successors
682             {
683 7     7 0 11 my ($self) = @_;
684            
685 7         9 my $num_successors = 0;
686 7         17 my $iterator = $self->get_descendants_num_iterator();
687              
688 7         13 while(my $next_descendant = $iterator->()){
689 7         13 $num_successors++;
690             }
691              
692 7         98 return $num_successors
693             }
694              
695              
696              
697              
698              
699             #-----------------------------------------------------------------------------
700             # Get descendants number function.
701             #
702             #
703             #
704             #-----------------------------------------------------------------------------
705             sub get_descendants_number
706             {
707 0     0 0 0 my ($phrase_obj) = @_;
708 0 0       0 if(!$phrase_obj){
709 0         0 return;
710             }
711            
712 0         0 my $words = $phrase_obj->{_word_list};
713 0         0 my $words_w_cands = $phrase_obj->{_words_w_cands_list};
714 0         0 my $dictionary = $phrase_obj->{_dictionary};
715 0         0 my $dictionary_rev = $phrase_obj->{_dictionary_rev};
716 0         0 my $repeated_pal_hash_ref = $phrase_obj->{_repeated_pal_hash_ref};
717 0         0 my $letters_seen = $phrase_obj->{_letters_seen};
718 0         0 my $cost = $phrase_obj->{_cost};
719 0         0 my $cost_so_far = $phrase_obj->{_cost_so_far};
720 0         0 my $num_chars_so_far = $phrase_obj->{_num_chars_so_far};
721 0         0 my $no_match_remainder = $phrase_obj->{_no_match_remainder};
722 0         0 my $depth = $phrase_obj->{_depth};
723            
724 0         0 my $direction = $phrase_obj->{_dir};
725 0         0 my $word = $phrase_obj->{_start_word};
726 0         0 my $whole_word = $phrase_obj->{_cand};
727 0         0 my $len_word = length($word);
728 0         0 my @cands;
729             my @descendants;
730              
731            
732 0 0       0 if($direction == 0){
    0          
733 0         0 @cands = AI::Pathfinding::SMAstar::Examples::PalUtils::get_cands_from_left($word, $dictionary, $dictionary_rev);
734             }
735             elsif($direction == 1){
736 0         0 @cands = AI::Pathfinding::SMAstar::Examples::PalUtils::get_cands_from_right($word, $dictionary, $dictionary_rev);
737             }
738            
739            
740 0         0 my @words_to_make_phrases;
741             my $stored_c;
742              
743 0         0 my $num_successors = 0;
744              
745 0         0 while(1){
746             # this is a continuation of the second case below, where there were no
747             # match-remainders for the phrase-so-far, i.e. the palindrome has a space
748             # in the middle with mirrored phrases on each side. 'cat tac' for example.
749 0         0 my $next_word = shift(@words_to_make_phrases);
750 0 0       0 if($next_word){
751            
752 0         0 my $w = $next_word;
753              
754 0         0 my $repeated_word_p = 0;
755 0         0 my $antecedent = $phrase_obj->{_predecessor};
756 0         0 my $antecedent_dir = $antecedent->{_dir};
757 0         0 while($antecedent){
758              
759 0 0 0     0 if(defined($antecedent->{_word}) && $w eq $antecedent->{_word} && $antecedent_dir == 0){
      0        
760 0         0 $repeated_word_p = 1;
761 0         0 last;
762             }
763 0         0 $antecedent = $antecedent->{_predecessor};
764 0 0       0 if($antecedent){
765 0         0 $antecedent_dir = $antecedent->{_dir};
766             }
767             }
768              
769 0 0 0     0 if($repeated_word_p || $w eq $word){
770 0         0 next; #skip this word, we are already looking at it
771             }
772 0         0 $num_successors++;
773            
774             }
775             else{
776 0         0 my $c = shift(@cands);
777 0 0       0 if(!$c){
778 0         0 return $num_successors;
779             }
780            
781             # ------------- filter for repeated palcands for a particular word------
782             # ----------------------------------------------------------------------
783             # This will avoid many repeated patterns among palindromes to trim down the
784             # number redundant palindromes generated.
785             #
786 0         0 my $letters_seen_str = join("", @{$phrase_obj->{_letters_seen}});
  0         0  
787 0 0       0 if($letters_seen_str){
788 0         0 my $repeated_pal_hash_key;
789 0         0 $repeated_pal_hash_key = $word . "^" . $c . "^" . $letters_seen_str;
790            
791             #print "\n\nrepeated_pal_hash_key: $repeated_pal_hash_key\n";
792 0 0       0 if(my $hash_val = $repeated_pal_hash_ref->{$repeated_pal_hash_key}){
793             # skip because '$word' <--> '$p' pattern has already appeared in a previous palindrome.
794 0 0       0 if($hash_val != $depth){
795 0         0 next; #skip
796             }
797             }
798             else{
799             #flag this candidate as already having been tested (below).
800 0         0 $repeated_pal_hash_ref->{$repeated_pal_hash_key} = $depth;
801             }
802             }
803             #--------------------------------------------------------------------------
804             #--------------------------------------------------------------------------
805            
806 0         0 my $len_c = length($c);
807 0         0 my $rev_c = reverse($c);
808 0         0 my $word_remainder;
809            
810 0 0       0 if($len_c < $len_word){
    0          
811 0         0 $word_remainder = $c;
812             }
813             elsif($len_c > $len_word){
814 0         0 $word_remainder = $word;
815             }
816 0         0 my $rev_word_remainder = reverse($word);
817            
818 0         0 my $num_cands = @cands;
819            
820 0         0 my $match_remainder;
821             my $len_match_remainder;
822            
823            
824            
825 0 0       0 if($len_c != $len_word){
826 0         0 $match_remainder = 1;
827             }
828            
829            
830 0 0       0 if($match_remainder){ # there is a length difference between the candidate and this word.
831 0         0 $num_successors++;
832             }
833             else{
834             #
835             # There is no match_remainder, so this candidate is the reverse
836             # of $word, or the phrase built so far is an "even" palindrome,
837             # i.e. it has a word boundary (space) in the middle.
838             #
839             #
840             # This is a special case since there is no match remainder.
841             # Because there is no remainder to create new phrase obj from, this
842             # section goes through the whole word list and creates phrase objects
843             # for each new word. The number of new characters offered by each
844             # word is recorded to help with guided search.
845             #
846             # Update: this case now only goes through the word list for which there
847             # are cands.
848            
849 0         0 @words_to_make_phrases = @$words_w_cands;
850             #@words_to_make_phrases = @$words;
851            
852            
853 0         0 $stored_c = $c;
854 0         0 my $next_word = shift(@words_to_make_phrases);
855 0         0 my $w = $next_word;
856            
857 0         0 my $repeated_word_p = 0;
858 0         0 my $antecedent = $phrase_obj->{_predecessor};
859 0         0 my $antecedent_dir = $antecedent->{_dir};
860 0         0 while($antecedent){
861            
862 0 0 0     0 if(defined($antecedent->{_word}) && $w eq $antecedent->{_word} && $antecedent_dir == 0){
      0        
863 0         0 $repeated_word_p = 1;
864 0         0 last;
865             }
866 0         0 $antecedent = $antecedent->{_predecessor};
867 0 0       0 if($antecedent){
868 0         0 $antecedent_dir = $antecedent->{_dir};
869             }
870             }
871            
872 0 0 0     0 if($repeated_word_p || $w eq $word){
873 0         0 next; #skip this word, we are already looking at it
874             }
875 0         0 $num_successors++;
876             }
877             }
878             }
879              
880 0         0 return $num_successors;
881              
882             }
883              
884              
885              
886             #-----------------------------------------------------------------------------
887             # Get descendants iterator function.
888             # Generate the next descendant of a phrase object. Each descendant adds
889             # another word to the phrase that could possibly lead to a palindrome
890             #
891             #-----------------------------------------------------------------------------
892             sub get_descendants_num_iterator
893             {
894 7     7 0 6 my ($phrase_obj) = @_;
895 7 50       15 if(!$phrase_obj){
896 0         0 return;
897             }
898            
899 7         10 my $words = $phrase_obj->{_word_list};
900 7         10 my $words_w_cands = $phrase_obj->{_words_w_cands_list};
901 7         7 my $dictionary = $phrase_obj->{_dictionary};
902 7         7 my $dictionary_rev = $phrase_obj->{_dictionary_rev};
903 7         8 my $repeated_pal_hash_ref = $phrase_obj->{_repeated_pal_hash_ref};
904 7         10 my $letters_seen = $phrase_obj->{_letters_seen};
905 7         9 my $cost = $phrase_obj->{_cost};
906 7         8 my $cost_so_far = $phrase_obj->{_cost_so_far};
907 7         7 my $num_chars_so_far = $phrase_obj->{_num_chars_so_far};
908 7         8 my $no_match_remainder = $phrase_obj->{_no_match_remainder};
909 7         13 my $depth = $phrase_obj->{_depth};
910            
911 7         8 my $direction = $phrase_obj->{_dir};
912 7         12 my $word = $phrase_obj->{_start_word};
913 7         7 my $whole_word = $phrase_obj->{_cand};
914 7         7 my $len_word = length($word);
915 7         8 my @cands;
916             my @descendants;
917              
918            
919 7 100       16 if($direction == 0){
    50          
920 5         13 @cands = AI::Pathfinding::SMAstar::Examples::PalUtils::get_cands_from_left($word, $dictionary, $dictionary_rev);
921             }
922             elsif($direction == 1){
923 2         8 @cands = AI::Pathfinding::SMAstar::Examples::PalUtils::get_cands_from_right($word, $dictionary, $dictionary_rev);
924             }
925            
926            
927 7         8 my @words_to_make_phrases;
928             my $stored_c;
929              
930             return sub{
931              
932 16     16   18 LABEL:
933             # this is a continuation of the second case below, where there were no
934             # match-remainders for the phrase-so-far, i.e. the palindrome has a space
935             # in the middle with mirrored phrases on each side. 'cat tac' for example.
936             my $next_word = shift(@words_to_make_phrases);
937 16 100       28 if($next_word){
938            
939 2         2 my $w = $next_word;
940              
941 2         2 my $repeated_word_p = 0;
942 2         4 my $antecedent = $phrase_obj->{_predecessor};
943 2         3 my $antecedent_dir = $antecedent->{_dir};
944 2         4 while($antecedent){
945              
946 2 0 33     10 if(defined($antecedent->{_word}) && $w eq $antecedent->{_word} && $antecedent_dir == 0){
      33        
947 0         0 $repeated_word_p = 1;
948 0         0 last;
949             }
950 2         2 $antecedent = $antecedent->{_predecessor};
951 2 50       7 if($antecedent){
952 0         0 $antecedent_dir = $antecedent->{_dir};
953             }
954             }
955              
956 2 50 33     9 if($repeated_word_p || $w eq $word){
957 0         0 goto LABEL;
958             #next; #skip this word, we are already looking at it
959             }
960 2         5 return 1;
961            
962             }
963             else{
964 14         17 my $c = shift(@cands);
965 14 100       21 if(!$c){
966 7         14 return;
967             }
968            
969             # ------------- filter for repeated palcands for a particular word------
970             # ----------------------------------------------------------------------
971             # This will avoid many repeated patterns among palindromes to trim down the
972             # number redundant palindromes generated.
973             #
974 7         8 my $letters_seen_str = join("", @{$phrase_obj->{_letters_seen}});
  7         19  
975 7 100       13 if($letters_seen_str){
976 3         3 my $repeated_pal_hash_key;
977 3         7 $repeated_pal_hash_key = $word . "^" . $c . "^" . $letters_seen_str;
978            
979             #print "\n\nrepeated_pal_hash_key: $repeated_pal_hash_key\n";
980 3 50       7 if(my $hash_val = $repeated_pal_hash_ref->{$repeated_pal_hash_key}){
981             # skip because '$word' <--> '$p' pattern has already appeared in a previous palindrome.
982 0 0       0 if($hash_val != $depth){
983 0         0 goto LABEL;
984             # next; #skip
985             }
986             }
987             else{
988             #flag this candidate as already having been tested (below).
989 3         147 $repeated_pal_hash_ref->{$repeated_pal_hash_key} = $depth;
990             }
991             }
992             #--------------------------------------------------------------------------
993             #--------------------------------------------------------------------------
994            
995 7         11 my $len_c = length($c);
996 7         13 my $rev_c = reverse($c);
997 7         5 my $word_remainder;
998            
999 7 100       18 if($len_c < $len_word){
    100          
1000 2         3 $word_remainder = $c;
1001             }
1002             elsif($len_c > $len_word){
1003 3         5 $word_remainder = $word;
1004             }
1005 7         12 my $rev_word_remainder = reverse($word);
1006            
1007 7         6 my $num_cands = @cands;
1008            
1009 7         8 my $match_remainder;
1010             my $len_match_remainder;
1011            
1012            
1013            
1014 7 100       13 if($len_c != $len_word){
1015 5         6 $match_remainder = 1;
1016             }
1017            
1018            
1019 7 100       13 if($match_remainder){ # there is a length difference between the candidate and this word.
1020 5         12 return 1;
1021             }
1022             else{
1023             #
1024             # There is no match_remainder, so this candidate is the reverse
1025             # of $word, or the phrase built so far is an "even" palindrome,
1026             # i.e. it has a word boundary (space) in the middle.
1027             #
1028             #
1029             # This is a special case since there is no match remainder.
1030             # Because there is no remainder to create new phrase obj from, this
1031             # section goes through the whole word list and creates phrase objects
1032             # for each new word. The number of new characters offered by each
1033             # word is recorded to help with guided search.
1034             #
1035             # Update: this case now only goes through the word list for which there
1036             # are cands.
1037            
1038 2         4 @words_to_make_phrases = @$words_w_cands;
1039             #@words_to_make_phrases = @$words;
1040            
1041            
1042 2         2 $stored_c = $c;
1043 2         3 my $next_word = shift(@words_to_make_phrases);
1044 2         3 my $w = $next_word;
1045            
1046 2         1 my $repeated_word_p = 0;
1047 2         6 my $antecedent = $phrase_obj->{_predecessor};
1048 2         8 my $antecedent_dir = $antecedent->{_dir};
1049 2         6 while($antecedent){
1050            
1051 2 0 33     7 if(defined($antecedent->{_word}) && $w eq $antecedent->{_word} && $antecedent_dir == 0){
      33        
1052 0         0 $repeated_word_p = 1;
1053 0         0 last;
1054             }
1055 2         4 $antecedent = $antecedent->{_predecessor};
1056 2 50       6 if($antecedent){
1057 0         0 $antecedent_dir = $antecedent->{_dir};
1058             }
1059             }
1060            
1061 2 50 33     10 if($repeated_word_p || $w eq $word){
1062 2         39 goto LABEL;
1063             #next; #skip this word, we are already looking at it
1064             }
1065 0         0 return 1;
1066             }
1067             }
1068             }
1069 7         66 }
1070              
1071              
1072              
1073              
1074              
1075              
1076              
1077              
1078              
1079              
1080              
1081              
1082              
1083              
1084              
1085              
1086              
1087              
1088              
1089              
1090              
1091              
1092              
1093              
1094              
1095              
1096             #-----------------------------------------------------------------------------
1097             # traverse from candidate phrase-object back up to start word, building up the
1098             # phrase string. iterative version.
1099             #-----------------------------------------------------------------------------
1100             sub roll_up_phrase
1101             {
1102 34     34 0 68 my ($pobj, $phrase, $depth) = @_; # depth == depth of recursion
1103              
1104 34 50       64 if(!$depth){
1105 34         39 $depth = 0;
1106             }
1107            
1108 34         57 while($pobj){
1109 45 100 100     151 if(!$pobj->{_cand} && $depth == 0){
1110             # top-level call to roll_up_phrase is called on a root node.
1111 12         36 return $pobj->{_start_word};
1112             }
1113             else{
1114             # if depth is 0, that means this is a top-level call.
1115             # otherwise this is the nth iteration within this while loop.
1116              
1117              
1118             # if this is a top-level call and _phrase is already defined,
1119             # just return _phrase.
1120 33 100 100     106 if(defined($pobj->{_phrase}) && !$depth){
1121 15         42 return $pobj->{_phrase};
1122             }
1123            
1124 18         23 my $direction = $pobj->{_dir};
1125 18         19 my $antecedent = $pobj->{_predecessor};
1126 18         15 my $antecedent_predecessor;
1127 18         18 my $no_match_remainder = $pobj->{_no_match_remainder};
1128 18         16 my $ant_direction = 0;
1129 18         17 my $ant_cand;
1130            
1131 18 100       33 if($antecedent){
1132 11         10 $antecedent_predecessor = $antecedent->{_predecessor};
1133 11         14 $ant_direction = $antecedent->{_dir};
1134 11         16 $ant_cand = $antecedent->{_cand};
1135             }
1136            
1137            
1138              
1139 18 100       36 my $word = defined($pobj->{_word}) ? $pobj->{_word} : "";
1140 18 50       31 my $startword = defined($pobj->{_start_word}) ? $pobj->{_start_word} : "";
1141 18 100       31 my $cand = defined($pobj->{_cand}) ? $pobj->{_cand} : "";
1142            
1143 18 100       30 if(!$phrase){
1144 7 100       17 if($direction == 0){
    50          
1145 2         4 $phrase = $cand;
1146             }
1147             elsif($direction == 1){
1148 5         8 $phrase = $cand;
1149             }
1150             }
1151             else{
1152 11 100       27 if($direction == 0){
    50          
1153 8 50       14 if($ant_direction == 0){
    0          
1154             #**** special case for root node descendant***
1155 8 50       14 if(!$antecedent_predecessor){ # antecedent is root node.
1156 8 50       19 if($word){
1157 8         20 $phrase = $word . " " . $phrase . " " . $cand;
1158             }
1159             else{
1160 0         0 $phrase = $phrase . " " . $cand;
1161             }
1162             }
1163             else{
1164 0 0       0 if($no_match_remainder){ # handle the case where there was no match remainder
1165 0         0 $phrase = $word . " " . $phrase . " " . $cand;
1166             }
1167             else{
1168 0         0 $phrase = "" . $phrase . " " . $cand;
1169             }
1170             }
1171             }
1172             elsif($ant_direction == 1){
1173 0 0       0 if($no_match_remainder){ # handle the case where there was no match remainder
1174 0         0 $phrase = $cand . " " . $word . " " . $phrase . "";
1175             }
1176             else{
1177 0         0 $phrase = $cand . " " . $phrase . "";
1178             }
1179             }
1180             }
1181             elsif($direction == 1){
1182 3 100       7 if($ant_direction == 0){
    50          
1183 2         6 $phrase = "" . $phrase . " " . $cand;
1184            
1185             }
1186             elsif($ant_direction == 1){
1187 1         3 $phrase = $cand . " " . $phrase . "";
1188             }
1189             }
1190             }
1191             }
1192            
1193 18         23 $pobj = $pobj->{_predecessor};
1194 18         30 $depth++;
1195            
1196             } # end while($pobj);
1197            
1198 7         15 return $phrase;
1199             }
1200              
1201              
1202              
1203              
1204             sub roll_up_phrase_plus_word
1205             {
1206 0     0 0 0 my ($self) = @_;
1207              
1208 0         0 my $phrase = $self->{_phrase};
1209 0         0 my $word = $self->{_start_word};
1210 0         0 my $phrase_plus_cand = $phrase . ": " . $word;
1211              
1212 0         0 return $phrase_plus_cand;
1213             }
1214              
1215              
1216              
1217              
1218             sub DESTROY
1219             {
1220 5     5   7 my ($self) = @_;
1221              
1222 5         14 my $antecedent;
1223             my $ant_phrase;
1224              
1225 5         15 my ($pkg, $filename, $line_num) = caller();
1226              
1227 5 100       17 if($self->{_predecessor}){
1228 4         5 $antecedent = $self->{_predecessor};
1229 4 100       13 $ant_phrase = $antecedent->{_phrase} ? $antecedent->{_phrase} : $antecedent->{_start_word};
1230             }
1231             else{
1232 1         2 $antecedent->{_phrase} = "none";
1233             }
1234              
1235             # print " $line_num, destroying phrase object $self, '" . $self->{_start_word} . ", " . $self->{_phrase} .
1236             # "', parent is $antecedent: '" . $ant_phrase . "' \n";
1237            
1238             # if($line_num != 0){ # if not final sweep at program exit
1239             # print " caller is: $pkg, $filename, $line_num\n";
1240             # }
1241            
1242 5 100       12 if($line_num == 0){ # line_num is zero
1243 3         5 $d++;
1244             # print "\$d : $d\n";
1245             }
1246            
1247             #${$self->{_predecessor}} = 0;
1248             #${$self->{_descendants_list}} = 0;
1249              
1250 5         223 delete $self->{_predecessor};
1251            
1252            
1253             }
1254              
1255              
1256              
1257              
1258              
1259              
1260              
1261              
1262              
1263              
1264              
1265              
1266              
1267              
1268              
1269              
1270              
1271              
1272              
1273              
1274              
1275              
1276              
1277              
1278              
1279              
1280              
1281              
1282              
1283              
1284              
1285              
1286              
1287             1; # so the require or use succeeds
1288