Perl Weekly Challenge: Week 336

Challenge 1:

Equal Group

You are given an array of integers.

Write a script to return true if the given array can be divided into one or more groups: each group must be of the same size as the others, with at least two members, and with all members having the same value.

Example 1
Input: @ints = (1,1,2,2,2,2)
Output: true

Groups: (1,1), (2,2), (2,2)
Example 2
Input: @ints = (1,1,1,2,2,2,3,3)
Output: false

Groups: (1,1,1), (2,2,2), (3,3)
Example 3
Input: @ints = (5,5,5,5,5,5,7,7,7,7,7,7)
Output: true

Groups: (5,5,5,5,5,5), (7,7,7,7,7,7)
Example 4
Input: @ints = (1,2,3,4)
Output: false
Example 5
Input: @ints = (8,8,9,9,10,10,11,11)
Output: true

Groups: (8,8), (9,9), (10,10), (11,11)

Once again, Raku comes through with a one-liner.

First we count the occurrences of each integer in the command-line arguments using the .classify() method. This create a Hash (specified by the :into argument) whose keys are distinct numbers and whose values are the number of times each occurred. In hindsight I could also have used .Bag which would have saved a few more characters. Then we find the greatest common divisor (GCD) of all counts (%c.values) with [gcd] and if it is greater or equal to 2, True is printed otherwise False.

@*ARGS.classify({ $_ }, :into(my %c)); say ([gcd] %c.values) >= 2

(Full code on Github.)

Perl doesn't have a standard gcd() function so we provide our own. This is a recursive function that takes two values and provides the greatest common denominator.

sub gcd($a, $b) {
    if ($b == 0) {
        return  $a;
    }

    gcd($b, $a % $b);
}

We also don't have .classify() so we emulate it by manually populating a hash with the frequency of individual elements in @ints.

my %c;
for my $int (@ints) {
    $c{$int}++;
}

Because our gcd() function only takes two values at a time, we need another loop to get the final GCD.

my $gcd = 0;
for my $v (values %c) {
    $gcd = gcd($gcd, $v);
}

If it's greater than or equal to two, we print true else false.

say $gcd >= 2 ? 'true' : 'false';

(Full code on Github.)

Challenge 2:

Final Score

You are given an array of scores by a team.

Write a script to find the total score of the given team. The score can be any integer, +, C or D. The + adds the sum of previous two scores. The score C invalidates the previous score. The score D will double the previous score.

Example 1
Input: @scores = ("5","2","C","D","+")
Output: 30

Round 1: 5
Round 2: 5 + 2
Round 3: 5 (invalidate the previous score 2)
Round 4: 5 + 10 (double the previous score 5)
Round 5: 5 + 10 + 15 (sum of previous two scores)

Total Scores: 30
Example 2
Input: @scores = ("5","-2","4","C","D","9","+","+")
Output: 27

Round 1: 5
Round 2: 5 + (-2)
Round 3: 5 + (-2) + 4
Round 4: 5 + (-2) (invalidate the previous score 4)
Round 5: 5 + (-2) + (-4) (double the previous score -2)
Round 6: 5 + (-2) + (-4) + 9
Round 7: 5 + (-2) + (-4) + 9 + 5 (sum of previous two scores)
Round 8: 5 + (-2) + (-4) + 9 + 5 + 14 (sum of previous two scores)

Total Scores: 27
Example 3
Input: @scores = ("7","D","D","C","+","3")
Output: 45

Round 1: 7
Round 2: 7 + 14 (double the previous score 7)
Round 3: 7 + 14 + 28 (double the previous score 14)
Round 4: 7 + 14 (invalidate the previous score 28)
Round 5: 7 + 14 + 21 (sum of previous two scores)
Round 6: 7 + 14 + 21 + 3

Total Scores: 45
Example 4
Input: @scores = ("-5","-10","+","D","C","+")
Output: -55

Round 1: (-5)
Round 2: (-5) + (-10)
Round 3: (-5) + (-10) + (-15) (sum of previous two scores)
Round 4: (-5) + (-10) + (-15) + (-30) (double the previous score -15)
Round 5: (-5) + (-10) + (-15) (invalidate the previous score -30)
Round 6: (-5) + (-10) + (-15) + (-25) (sum of previous two scores)

Total Scores: -55
Example 5
Input: @scores = ("3","6","+","D","C","8","+","D","-2","C","+")
Output: 128

Round  1: 3
Round  2: 3 + 6
Round  3: 3 + 6 + 9 (sum of previous two scores)
Round  4: 3 + 6 + 9 + 18 (double the previous score 9)
Round  5: 3 + 6 + 9 (invalidate the previous score 18)
Round  6: 3 + 6 + 9 + 8
Round  7: 3 + 6 + 9 + 8 + 17 (sum of previous two scores)
Round  8: 3 + 6 + 9 + 8 + 17 + 34 (double the previous score 17)
Round  9: 3 + 6 + 9 + 8 + 17 + 34 + (-2)
Round 10: 3 + 6 + 9 + 8 + 17 + 34 (invalidate the previous score -2)
Round 11: 3 + 6 + 9 + 8 + 17 + 34 + 51 (sum of previous two scores)

Total Scores: 128

This solution is longer than one line but quite straightforward. In essence we are just writing down the spec in code.

We will be adding and potentially removing scores from the list we will finally work with and the best data structure for that kind of thing is a stack. We use a Perl Array for this.

my @previous; 

For each score...

for @scores -> $score {
    given $score {

...it being a number will be the most common situation. In this case we just add it to the stack.

        default {
            @previous.push($score);
        }

Otherwise if it is one of the three special characters, we just perform whatever operations the spec described and adjust the stack accordingly.

        when 'C' {
            @previous.pop;
         }
        when 'D' {
            @previous.push(2 * @previous[*-1]);
        }
        when '+' {
            @previous.push(@previous[*-1] + @previous[*-2]);

        }
    } 
}

Finally we take the .sum() of the stack and print it out.

say @previous.sum;

(Full code on Github.)

For the Perl version, we have to provide our own sum() function. While Perl does have given/when they have been deprecated and will be removed by the time I next upgrade my version of Perl. So instead I used a series of if/elses.

my @previous; 

for my $score (@scores) {
    if ($score eq 'C') {
        pop @previous;
    }
    elsif ($score eq 'D') {
        push @previous, 2 * $previous[-1];
    }
    elsif ($score eq '+') {
        push @previous, $previous[-1] + $previous[-2];
    }
    else {
        push @previous, $score;
    }
}

say sum(@previous);

(Full code on Github.)