Perl Weekly Challenge 151.

My solutions (task 1 and task 2 ) to the The Weekly Challenge - 151.

Task 1: Binary Tree Depth

Submitted by: Mohammad S Anwar
You are given binary tree.

Write a script to find the minimum depth.

The minimum depth is the number of nodes from the root to the
nearest leaf node (node without any children).

Example 1:
Input: '1 | 2 3 | 4 5'

                1
               / \
              2   3
             / \
            4   5

Output: 2

Example 2:
Input: '1 | 2 3 | 4 *  * 5 | * 6'

                1
               / \
              2   3
             /     \
            4       5
             \
              6
Output: 3

One could solve this task by building the tree and then running through it breath-first until a leaf is found, or depth-first noting the minimum depth among subtrees. However, given the input format as a sequence of rows, the task is much easier, as it is already half solved. If a row has a pair of missing siblings, that row is beyond the tree’s minimum depth. Thus, I can solve the task with the following one liner.1

perl -E 'for(@ARGV){@l=split /\s*\|\s*/; $d=0; foreach(@l){s/((\S)+)/$2/g; s/\s+//g;
$_.=("*" x(2**$d-length)); last if m/^(..)*(\*\*)/; ++$d;} say "In: $_\nOut: $d";}
' '1 | 2 3 | 4 5' '1 | 2 3 | 4 * * 5 | * 6'

Results:

In: 1 | 2 3 | 4 5
Out: 2
In: 1 | 2 3 | 4 * * 5 | * 6
Out: 3

I can better explain the program with its full version.

 1  # Perl weekly challenge 151
 2  # Task 1: Binary tree depth
 3  #
 4  # See https://wlmb.github.io/2022/02/07/PWC151/#task-1-binary-tree-depth
 5  use v5.12;
 6  use warnings;
 7  use Try::Tiny;
 8  die "Usage: ./ch-1.pl T1 [T2]...\n"
 9      . "where Ti are trees of the form 'R1 | R2...'\n"
10      . "and each row consists of nodes (strings) or an asterisk * (empty node)\n"
11      unless @ARGV;
12  for my $tree (@ARGV){
13      my @rows=split /\s*\|\s*/, $tree; # separate into rows.
14      my $depth=0;   # Depth of first row is 1. This is above the first row
15      try {
16          foreach(@rows){
17              s/((\S)+)/$2/g; # replace contiguous characters by first
18              s/\s+//g; # remove spaces
19              die "\n" if length > 2**$depth; # row can't be larger than 2**depth
20              $_.=("*" x(2**$depth-length)); # Fill row with asterisks if necessary
21              # Two consecutive asterisks at even-odd position mean we are below a leave
22              # so we have finished our search;
23              last if m/^(..)*(\*\*)/;
24              ++$depth;  # increase and iterate
25          }
26          say "Input: $tree\nOutput: $depth";
27      }
28      catch { say "A row is too long in $tree";}
29  }

The idea is to change each row into a string with single letters for each node and asterisks for empty nodes. I examine the nodes with a regular expression, two at a time. If both are asterisks, then the node immediately above is a leaf and we are beyond the minimum height of the tree. I iterate over all provided trees and use Try::Tiny to catch errors and skip to next tree.

Examples:

./ch-1.pl  '1 | 2 3 | 4 5' '1 | 2 3 | 4 * * 5 | * 6'

Results:

Input: 1 | 2 3 | 4 5
Output: 2
Input: 1 | 2 3 | 4 * * 5 | * 6
Output: 3

Examples with limit cases and errors (empty tree, just the root, balanced tree, extra separator and long rows):

./ch-1.pl  '' '1' '1|2 3| 3 4 5 6' '1|2 3| 3 4 5 6|' '1 2' '1| 2 3 4'

Results:

Input:
Output: 0
Input: 1
Output: 1
Input: 1|2 3| 3 4 5 6
Output: 3
Input: 1|2 3| 3 4 5 6|
Output: 3
A row is too long in 1 2
A row is too long in 1| 2 3 4

Another example:

./ch-1.pl   '1 | 2 3 | 4 *  * 5 | * 6 * * * * 7 8'

Results:

Input: 1 | 2 3 | 4 *  * 5 | * 6 * * * * 7 8
Output: 3

This is wrong! The correct result should have been 4. The problem is that there are missing nodes in level 3 which therefore have empty descendants. I should have also checked that descendants have a parent. Unfortunately, I didn’t realize the error until I read Colin’s review. In the new version, I check that when I find two asterisks that the corresponding char in the previous line was not an asterisk. Below is a four-liner solution.

perl -d -E 'for(@ARGV){@l=split /\s*\|\s*/;$d=0; $o="";ROW:foreach(@l){
s/((\S)+)/$2/g; s/\s+//g; $_.=("*" x(2**$d-length));$n=$_;
while(s/^((..)*)\*\*/$1../){last ROW unless substr($o,length($1)/2,1) eq "*";
} $o=$n; ++$d;} say "In: $_\nOut: $d";}
' '1 | 2 3 | 4 5' '1 | 2 3 | 4 * * 5 | * 6' '1 | 2 3 | 4 *  * 5 | * 6 * * * * 7 8'

Results:

In: 1 | 2 3 | 4 5
Out: 2
In: 1 | 2 3 | 4 * * 5 | * 6
Out: 3
In: 1 | 2 3 | 4 *  * 5 | * 6 * * * * 7 8
Out: 4

So the problem seems to have been corrected.

A full version is

 1  # Perl weekly challenge 151
 2  # Task 1: Binary tree depth
 3  #
 4  # See https://wlmb.github.io/2022/02/07/PWC151/#task-1-binary-tree-depth
 5  use v5.12;
 6  use warnings;
 7  use Try::Tiny;
 8  die "Usage: ./ch-1a.pl T1 [T2]...\n"
 9      . "where Ti are trees of the form 'R1 | R2...'\n"
10      . "and each row consists of nodes (strings) or an asterisk * (empty node)\n"
11      unless @ARGV;
12  for my $tree (@ARGV){
13      my @rows=split /\s*\|\s*/, $tree; # separate into rows.
14      my $depth=0;   # Depth of first row is 1. This is above the first row
15      try {
16          my $old='';
17        ROW:
18          foreach(@rows){
19              s/((\S)+)/$2/g; # replace contiguous characters by first
20              s/\s+//g; # remove spaces
21              die "A row is too long in $tree\n"
22                  if length > 2**$depth; # row can't be larger than 2**depth
23              $_.=("*" x(2**$depth-length)); # Fill row with asterisks if necessary
24              my $new=$_;
25              # A non-empty node below an empty one is an error.
26              while(s/^((\*\*)*)([^\*].|.[^\*])/$1\*\*/){
27                  die "An empty node may not have descendants in $tree\n"
28                     if substr($old,length($1)/2,1) eq "*";
29              }
30              # Two empty nodes below a non-empty node mean the search is over.
31              $_=$new;
32              while(s/^((..)*)\*\*/$1../){
33                  last ROW unless substr($old,length($1)/2,1) eq "*";
34              }
35              $old=$new;
36              ++$depth;
37        }
38          say "Input: $tree\nOutput: $depth";
39      }
40      catch { say $_;}
41  }

Examples:

./ch-1a.pl  '1 | 2 3 | 4 5' '1 | 2 3 | 4 * * 5 | * 6'

Results:

Input: 1 | 2 3 | 4 5
Output: 2
Input: 1 | 2 3 | 4 * * 5 | * 6
Output: 3

Examples with limit cases and errors (empty tree, just the root, balanced tree, extra separator and long rows):

./ch-1a.pl  '' '1' '1|2 3| 3 4 5 6' '1|2 3| 3 4 5 6|' '1 2' '1| 2 3 4'

Results:

Input:
Output: 0
Input: 1
Output: 1
Input: 1|2 3| 3 4 5 6
Output: 3
Input: 1|2 3| 3 4 5 6|
Output: 3
A row is too long in 1 2
A row is too long in 1| 2 3 4

An example with empty nodes with non-empty descendants:

./ch-1a.pl   '1 | 2 3 | 4 *  * 5 | * 6 7 * * 8 * 9 '

Results:

An empty node may not have descendants in 1 | 2 3 | 4 *  * 5 | * 6 7 * * 8 * 9

An example with empty nodes with empty descendants:

./ch-1a.pl   '1 | 2 3 | 4 *  * 5 | * 6 * * * * 7 8'

Results:

Input: 1 | 2 3 | 4 *  * 5 | * 6 * * * * 7 8
Output: 4

Task 2: Rob The House

Submitted by: Mohammad S Anwar
You are planning to rob a row of houses, always starting with
the first and moving in the same direction. However, you can’t
rob two adjacent houses.

Write a script to find the highest possible gain that can be
achieved.

Example 1:
Input: @valuables = (2, 4, 5);
Output: 7

If we rob house (index=0) we get 2 and then the only house we
can rob is house (index=2) where we have 5.
So the total valuables in this case is (2 + 5) = 7.

Example 2:
Input: @valuables = (4, 2, 3, 6, 5, 3);
Output: 13

The best choice would be to first rob house (index=0) then rob
house (index=3) then finally house (index=5).
This would give us 4 + 6 + 3 =13.

The problem can be solved recursively: To find the best way to rob a list L0 of N houses 0..N-1 I first calculate the best way to rob the houses L1=1..N-1 and L2=2..N-1. I compare the bounty from list L1 to that of L2 plus the valuables of house 0 and then decide if I should skip or not house 0. This yields a simple oneliner:

perl -E 'sub r {my $x=shift; return 0 unless defined $x; return $x unless @_;
my ($y,$z)=(r(@_),$x+r(@_[1..@_-1])); $z>$y?$z:$y;} say "Input: ", join " ", @ARGV,
"\nOutput: ", r(@ARGV);' 2 4 5
perl -E 'sub r {my $x=shift; return 0 unless defined $x; return $x unless @_;
my ($y,$z)=(r(@_),$x+r(@_[1..@_-1])); $z>$y?$z:$y;} say "Input: ", join " ", @ARGV,
"\nOutput: ", r(@ARGV);' 4 2 3 6 5 3

Results:

Input: 2 4 5
Output:  7
Input: 4 2 3 6 5 3
Output:  13

Here the subroutine r receives as argument a list of houses and recursively determines the largest value that can be obtained.

With a small additional effort I can also print the sequence of houses to visit.

perl -E '@v=@ARGV; ($v,@c)=r(0); say "Input: ", join(" ", @v) ,"\nOutput: $v Houses: "
, join ", ", @c;sub r{my $c0=shift; my $v0=$v[$c0]; return (0) if $c0>=@v;
return ($v[$c0], $c0) if $c0==@v-1; my ($v1, @c1)=r($c0+1); my ($v2, @c2)=r($c0+2);
my $v3=$v0+$v2;  $v3>$v1?($v3, $c0, @c2):($v1, @c1);} ' 2 4 5
perl -E '@v=@ARGV; ($v,@c)=r(0); say "Input: ", join(" ", @v) ,"\nOutput: $v Houses: "
, join ", ", @c;sub r{my $c0=shift; my $v0=$v[$c0]; return (0) if $c0>=@v;
return ($v[$c0], $c0) if $c0==@v-1; my ($v1, @c1)=r($c0+1); my ($v2, @c2)=r($c0+2);
my $v3=$v0+$v2;  $v3>$v1?($v3, $c0, @c2):($v1, @c1);} ' 4 2 3 6 5 3

Results:

Input: 2 4 5
Output: 7 Houses: 0, 2
Input: 4 2 3 6 5 3
Output: 13 Houses: 0, 3, 5

In this case I pass as argument to the routine r the index of the first remaining house to consider, instead of a list of remaining houses. The return value is the sum of the robbed valuables and the list of visited houses.

The full version of the last program would be

 1  # Perl weekly challenge 151
 2  # Task 2: Rob the house
 3  #
 4  # See https://wlmb.github.io/2022/02/07/PWC151/#task-2-rob-the-house
 5  use v5.12;
 6  use warnings;
 7  use Memoize;
 8  memoize("optimize");
 9  die "Usage: ./ch-1.pl V0 [V1]...\n"
10      . "to optimize the robery of houses 0, 1,... with valuables V0, V1..."
11      unless @ARGV;
12  my @values=@ARGV;
13  my ($value,@houses)=optimize(0);
14  say "Input: ", join ", ", @values;
15  say "Output: $value";
16  say "Houses: ", join ", ", @houses;
17  sub optimize {
18      my $first=shift;
19      my $value=$values[$first];
20      return (0) if $first >= @values; # No more houses
21      return ($value, $first) if $first==@values-1; # Only one house left
22      my ($v1, @h1)=optimize($first+1); # what if I skip first house?
23      my ($v2, @h2)=optimize($first+2,); # what if I rob first and skip next?
24      my $v3=$value+$v2;
25      $v3>$v1 # which option is best?
26          ?($v3, $first, @h2) # First one and skip next
27          :($v1, @h1); # or skip first one
28  }

Examples:

./ch-2.pl 2 4 5
./ch-2.pl 4 2 3 6 5 3

Results:

Input: 2, 4, 5
Output: 7
Houses: 0, 2
Input: 4, 2, 3, 6, 5, 3
Output: 13
Houses: 0, 3, 5

In the case of a large list of houses, there are many repeated calls to optimize with the same argument. To avoid duplicate calculations I use the Memoize module. For just 30 houses it yields a speed increase of about a hundred-fold, and for 36 houses it is more than a thousand-fold.

Large example:

perl -E 'say sprintf "%.0f", 100*rand for(0..99)'|xargs ./ch-2.pl

Results (formatted):

Input: 76, 19, 48, 24, 28, 41, 13, 49, 59, 53, 87, 69, 93, 13, 74,
       58, 14, 83, 52, 29, 71, 95, 91, 67, 73, 32, 38, 63,
       3, 75, 71, 3, 34, 38, 65, 4, 90, 35, 44, 44, 93, 68,
       28, 36, 73, 58, 80, 71, 40, 86, 67, 46, 75, 69, 48,
       49, 18, 82, 33, 35, 37, 68, 0, 25, 65, 33, 4, 52, 72,
       78, 84, 52, 90, 52, 7, 52, 77, 84, 32, 8, 13, 51, 36,
       28, 27, 66, 71, 76, 42, 55, 85, 39, 54, 59, 39, 60,
       99, 26, 68, 84
Output: 2917
Houses: 0, 2, 5, 8, 10, 12, 14, 17, 20, 22, 24, 27, 29, 32, 34,
        36, 38, 40, 42, 44, 46, 49, 51, 53, 55, 57, 59, 61, 64, 66,
        68, 70, 72, 75, 77, 79, 81, 83, 85, 87, 90, 92, 94, 96, 99

After reading Colin’s review I realized that an alternative meaning of starting with the first is to always rob the house with index 0 (and then, to never rob that with index 1). I did interpret that phrase as start to consider which houses to rob starting with index 0.

Footnotes

1 There is a mistake. See below

Written on February 7, 2022