Perl Weekly Challenge: Week 93

Challenge 1:

Max Points

You are given set of co-ordinates @N.

Write a script to count maximum points on a straight line when given co-ordinates plotted on 2-d plane.

Example 1
|
|     x
|   x
| x
+ _ _ _ _

Input: (1,1), (2,2), (3,3)
Output: 3
Example 2
|
|
| x       x
|   x
| x   x
+ _ _ _ _ _

Input: (1,1), (2,2), (3,1), (1,3), (5,3)
Output: 3

I assume we are getting the co-ordinates from the command-line as comma-seperated pairs. For example 1, the input looks like this: 1,1 2,2 3,3. The first order of business is to convert the input into an array of 2-element arrays ussing .split() and .map().

my @points = @N.map({ [ $_.split(q{,}) ] });

The result will be the maximum number of points that make a straight line so we need a variable to store it. Because we have at least one point, we can start it at 1.

my $maxPoints = 1;

For each point, we (figuratively) draw lines to every other point via a double for-loop. We will store line information in as keys of a Hash whose value will be the number of times that line occurs.

for @points.keys -> $i {
    my %lines;

    for @points.keys -> $j {

If the origin point is the same as the destination point we can ignore it and move on to the next destination point.

        if $i == $j {
            next;
        }

We extract the x and y co-ordinates from the origin and destination points and find the delta of each.

        my ($x1, $y1) = @points[$i];
        my ($x2, $y2) = @points[$j];
        my ($dx, $dy) = ($x2 - $x1, $y2 - $y1);

We can get the slope of the line by dividing $dy by $dx but only if $dx is not 0. If it is, the line is vertical so it is added under the special key 'infinity'.

        if $dx == 0 {
            %lines{∞}++;
        } else {

If $dx is not 0, we can proceed with finding the slope of the line. Using this, we can also find the intercept. Together the slope and intercept provide enough information to use as a unique key in %lines to count how many points share the same line.

            my $slope = $dy / $dx;
            my $intercept = $y1 - $slope * $x1;
            %lines{"$slope,$intercept"}++;
        }
    }

After processing all destination points for a given origin point, we can determin the maximum number of points on any line passing through that origin which we store as $localMax. The // 0 is to allow the possibility that the origin point is completely isolated (i.e. no other points are collinear with it.) We also have to add 1 to account for the origin point itself.

    my $localMax = (%lines.values.max // 0) + 1;

It then updates the overall maximum ($maxPoints) if a higher count is found. A subtle point is that the code adds 1 to $localMax when updating $maxPoints, accounting for the anchor point itself.

    $maxPoints = ($maxPoints, $localMax).max;
}

Finally, we print the result, which is the largest number of points that are collinear.

say $maxPoints;

(Full code on Github.)

This is the Perl version. We don't have max() so we have to provide my own. I had an implementation from previous challenges but it only worked on two values so I wrote a different version that works on an entire array.

sub max(@values) {
    my $maximum = shift @values;

    while (my $next = shift @values) {
        if ($next > $maximum) {
            $maximum = $next;
        }
    }

    return $maximum;
}

The rest of the code is pretty much the same as in Raku.

my @points = (map { [split /,/] } @N);
my $maxPoints = 1;

for my $i (keys @points) {
    my %lines;

    for my $j (keys @points) {

        if ($i == $j) {
            next;
        }

        my ($x1, $y1) = @{$points[$i]};
        my ($x2, $y2) = @{$points[$j]};
        my ($dx, $dy) = ($x2 - $x1, $y2 - $y1);

        if ($dx == 0) {
            $lines{'inf'}++;
        } else {
            my $slope = $dy / $dx;
            my $intercept = $y1 - $slope * $x1;
            $lines{"$slope,$intercept"}++;
        }
    }

    my $localMax = (max(values %lines) // 0) + 1;

    $maxPoints = max($maxPoints, $localMax);
}

say $maxPoints;

(Full code on Github.)

Challenge 2:

Sum Path

You are given binary tree containing numbers `0-9` only.

Write a script to sum all possible paths from root to leaf.
Example 1
Input:
     1
   /
 2
 / \
3   4

Output: 13
as sum two paths (1->2->3) and (1->2->4)
Example 2
Input:
     1
    / \
   2   3
  /   / \
 4   5   6

Output: 26
as sum three paths (1->2->4), (1->3->5) and (1->3->6)

Conceptually, this challenge is simple. We need to do a depth-first search to each leaf node of the binary tree adding the value of each node along the way. The most difficult part for me was getting the input into binary tree form. I chose to have the command-line arguments to the script represent node values in level order with - as a placeholder for missing nodes. This is done in the makeTree() function which will be explained below. The tree produced by this function is an input, along with the value of the current node (which is 0) and the current value of the sum (which is also 0 initially,) to the sumPaths() function which will return the result. This will also be explained below. Thus the MAIN() function is very simple:

say sumPaths(makeTree(@nodes), 0, 0);

I have mentioned the binary tree has nodes but what exactly is a node? I have modelled a node as a Raku class with three fields.

class Node {

The value contained in the Node. It is set in stone when a Node object is created.

    has $.val;

The left and right children of this node if any. These are also Node objects and can be set after the object is created as shown by the is rw attribute.

    has Node $.left is rw;
    has Node $.right is rw;
}

Raku will take care of creating any neccessary getters and setters for us.

makeTree() constructs a binary tree from an array of values.

sub makeTree(@values) {

First any empty tree is created as an Array of Nodes.

my Node @tree;

For each value...

for @values.keys -> $i {

...if the value is -, that slot in the @tree array is left empty...

    if @values[$i] eq q{-} {
        next;
    }

...otherwise a Node object is created in that slot, containing that value.

    @tree[$i] = Node.new(val => @values[$i]);
}

Then, we link each node to its left and right children based on their positions in the array, following the standard binary heap indexing: left child at 2 * $i + 1, right child at 2 * $i + 2.

for @tree.keys -> $i {

(skipping empty spaces of course.)

    if (!@tree[$i]) {
        next;
    }

    my $l = 2 * $i + 1;
    my $r = 2 * $i + 2;

    if $l <= @tree.elems - 1 && @tree[$l] {
        @tree[$i].left = @tree[$l];
    }

    if $r <= @tree.elems - 1 && @tree[$r] {
        @tree[$i].right = @tree[$r];
    }
}

Finally, function returns the root node of the constructed tree.

return @tree[0];

}

The sumPaths() subroutine recursively traverses the tree.

sub sumPaths(Node $node, $num is copy, $sum is copy) {

Every recursive function needs a halting condition so it does not continue for ever (or until stack space runs out.) For sumpaths() that happens when the current node is empty and in that case, the current value of $sum is returned.

    if !$node {
        return $sum;
    }

We accumulate the value of each node along the current path in $num.

    $num += $node.val;

When a leaf node is reached (no left or right child), We add the accumulated path value to $sum.

    if !$node.left && !$node.right {
        $sum += $num;

Otherwise, for non-leaf nodes, we continue the traversal for both left and right children.

    } else {
        $sum = sumPaths($node.left, $num, $sum);
        $sum = sumPaths($node.right, $num, $sum);
    }

The recursion ensures all root-to-leaf paths are considered, and when that is done, final sum is returned.

    return $sum;
}

(Full code on Github.)

Until now, I would have used something like the Moo module rather than Perls' builtin OOP facilities but in modern versions of Perl, the builtin support has improved markedly. See perldoc perlclass for details.

In order to use this, we have to include this line in our script.

use feature qw/ class /;

Because this is still considered an experimental feature, we also need this line so Perl doesn't complain about us using it.

no warnings qw/ experimental::class /;

Now we can create a class like this. The :param attribute means that an initial value for that field is required when an object of that class is constructed.

class Node {
    field $val :param;
    field $left;
    field $right;

Unfortunately in Perl v5.38 which I am using, support for automatic generation of getters and setters is missing but it is simple to create them ourselves.

    method val { return $val; }
    method left { return $left; }
    method right { return $right; }

    method setLeft($node) { $left = $node; }
    method setRight($node) { $right = $node; }
}

The rest of the code is very close to the Raku version.

sub makeTree(@values) {
    my Node @tree;

    for my $i (keys @values) {
        if ($values[$i] eq q{-}) {
            next;
        }
        $tree[$i] = Node->new(val => $values[$i]);
    }

    for my $i (keys @tree) {
        if (!$tree[$i]) {
            next;
        }

        my $l = 2 * $i + 1;
        my $r = 2 * $i + 2;

        if ($l <= scalar @tree - 1 && $tree[$l]) {
            $tree[$i]->setLeft($tree[$l]);
        }

        if ($r <= scalar @tree - 1 && $tree[$r]) {
            $tree[$i]->setRight($tree[$r]);
        }
    }

    return $tree[0];
}

sub sumPaths($node, $num, $sum) {
    if (!$node) {
        return $sum;
    }

    $num += $node->val;

    if (!$node->left && !$node->right) {
        $sum += $num;
    } else {
        $sum = sumPaths($node->left, $num, $sum);
        $sum = sumPaths($node->right, $num, $sum);
    }

    return $sum;
}

say sumPaths(makeTree(@nodes), 0, 0);

(Full code on Github.)