Perl Weekly Challenge: Week 270

Challenge 1:

Special Positions

You are given a m x n binary matrix.

Write a script to return the number of special positions in the given binary matrix.

A position (i, j) is called special if $matrix[i][j] == 1 and all other elements in the row i and column j are 0.

Example 1
Input: $matrix = [ [1, 0, 0],
                   [0, 0, 1],
                   [1, 0, 0],
                 ]
Output: 1

There is only one special position (1, 2) as $matrix[1][2] == 1
and all other elements in row 1 and column 2 are 0.
Example 2
Input: $matrix = [ [1, 0, 0],
                   [0, 1, 0],
                   [0, 0, 1],
                 ]
Output: 3

Special positions are (0,0), (1, 1) and (2,2).

The matrix was created from the command-line. Each argument represents a row in the matrix; a string consisting of integers separeated by spaces. These are the columns. So we can slurp in the whole matrix with .map() and .words().

my @matrix = @args.map({ [$_.words] });

We also need to store the number of special positions found.

my $specials = 0;

The naive way to determine if a position is special is to check if the number there is 1 and if so, check that the numbers in the rest of the row and column are all 0. But then I thought of an optimization. If the number in a position is 1 it will be special if the sum of all the numbers in its row is 1 as is the sum of all the numbers in its column. This is easy to implement in code.

We go through all the rows and columns of the matrix looking for 1s.

for @matrix.keys -> $row {
    for @matrix[$row].keys -> $col {

If a 1 is found...

        if @matrix[$row;$col] == 1 &&

The sum of the values in the row is compared to 1 using .sum() on an "array slice."

        @matrix[$row;*].sum == 1 && 

The same is done for the column.

        @matrix[*;$col].sum == 1 {

If both are true, this is a special position so $specials is incremented.

            $specials++;
        }
    }
}

Finally, we print the number of special positions found.

say $specials;

(Full code on Github.)

The Perl version is pretty much the same. We have to provide our own implementation of sum() but I already had that from previous challenges.

my @matrix = map { [split /\s+/, $_] } @ARGV;
my $specials = 0;

for my $row (keys @matrix) {
    for my $col (keys @{$matrix[$row]}) {
        if (($matrix[$row]->[$col] == 1) &&
        (sum(@{$matrix[$row]}) == 1) &&
        (sum(map { $matrix[$_]->[$col] } keys @matrix) == 1)) {
            $specials++;
        }
    }
}

say $specials;

(Full code on Github.)

Challenge 2:

Distribute Elements

You are give an array of integers, @ints and two integers, $x and $y.

Write a script to execute one of the two options:

Level 1:
Pick an index i of the given array and do $ints[i] += 1

Level 2:
Pick two different indices i,j and do $ints[i] +=1 and $ints[j] += 1.

You are allowed to perform as many levels as you want to make every elements in the given array equal. There is cost attach for each level, for Level 1, the cost is $x and $y for Level 2.

In the end return the minimum cost to get the work done.

Example 1
Input: @ints = (4, 1), $x = 3 and $y = 2
Output: 9

Level 1: i=1, so $ints[1] += 1.
@ints = (4, 2)

Level 1: i=1, so $ints[1] += 1.
@ints = (4, 3)

Level 1: i=1, so $ints[1] += 1.
@ints = (4, 4)

We perforned operation Level 1, 3 times.
So the total cost would be 3 x $x => 3 x 3 => 9
Example 2
Input: @ints = (2, 3, 3, 3, 5), $x = 2 and $y = 1
Output: 6

Level 2: i=0, j=1, so $ints[0] += 1 and $ints[1] += 1
@ints = (3, 4, 3, 3, 5)

Level 2: i=0, j=2, so $ints[0] += 1 and $ints[2] += 1
@ints = (4, 4, 4, 3, 5)

Level 2: i=0, j=3, so $ints[0] += 1 and $ints[3] += 1
@ints = (5, 4, 4, 4, 5)

Level 2: i=1, j=2, so $ints[1] += 1 and $ints[2] += 1
@ints = (5, 5, 5, 4, 5)

Level 1: i=3, so $ints[3] += 1
@ints = (5, 5, 5, 5, 5)

We perforned operation Level 1, 1 time and Level 2, 4 times.
So the total cost would be (1 x $x) + (4 x $y) => (1 x 2) + (4 x 1) => 6

It took me a long time before I figured out how to solve this one and I'm still not a 100% sure I have the optimal answer.

My thinking is as we don't have a way to decrease the value of an element, we should find the largest value and keep adding to the others until they reach that. Furthermore we should try and do level 2 additions as often as possible because the cost is cheaper per element than level 1. (In hindsight this is only true if $y is less than twice %x. My code doesn't deal with this but it doesn't matter for the examples.)

We get the input from the command-line. The first to parameters are $x and $y. The rest are @ints.

First we find the largest value in @ints and store it for later use.

my $max = @ints.max;

We make a copy of @ints because it is immutable in Raku.

my @unequals = @ints;

And a variable is created to store the cost.

my $cost = 0;

Now we loop indefinitely.

loop {

We remove any elements from @unequals which are already at the $max value.

    @unequals = @unequals.grep({ $_ != $max });

We store the length of @unequals.

    my $remaining = @unequals.elems;

If the length is greater than 1...

    if $remaining > 1 {

...we do a level 2 operation by picking two random indices from @unequals with .pick() and incrementing their elements. In order to increment both the picked elements in one go, I used the "hyper" form of the ++ increment operator by placing » in front of it.

        @unequals[@unequals.keys.pick(2)]»++;

The cost is increased by the value of $y.

        $cost += $y;

If there is only 1 element remaining...

    } elsif $remaining == 1 {

...we increment it. (I originally had .pick(1) here but realized it was redundant.)

        @unequals[0]++;

The cost is increased by the value of $x.

        $cost += $x;

If there are no more elements remaining we break out of the loop.

    } else {
        last;
    }
}

Finally we print out the cost.

say $cost;

(Full code on Github.)

For the Perl version, I had to provide implementations of max() and pick(), I already had them from previous challenges but I updated them a little for Perl 5.38 and above, most notably the usage of signatures so here they are again.

sub max(@arr) {
    my $highest = '-inf';
    for my $i (@arr) {
        if ($i > $highest) {
            $highest = $i;
        }
    }

    return $highest;
}

sub pick($count, @arr) {
    my %results;
    my $picked = 0;

    while ($picked < $count) {
        my $random = $arr[int(rand(scalar @arr))];
        unless (exists $results{$random}) {
            $results{$random} = 1;
            $picked++;
        }
    }

    return wantarray ? keys %results : [ keys %results ];
}

Armed with these, we can do a faithful translation of the Raku version.

my $max = max(@ints);
my @unequals = @ints;
my $cost = 0;

while(1) {
    @unequals = grep { $_ != $max } @unequals;
    my $remaining = scalar @unequals;

    if ($remaining > 1) {
        for my $pick (pick(2, keys @unequals)) {
            $unequals[$pick]++;
        }
        $cost += $y;

    } elsif ($remaining == 1) {
        $unequals[0]++;
        $cost += $x;

    } else {
        last;
    }
}

say $cost;

(Full code on Github.)