Perl Weekly Challenge: Week 335

Challenge 1:

Common Characters

You are given an array of words.

Write a script to return all characters that is in every word in the given array including duplicates.

Example 1
Input: @words = ("bella", "label", "roller")
Output: ("e", "l", "l")
Example 2
Input: @words = ("cool", "lock", "cook")
Output: ("c", "o")
Example 3
Input: @words = ("hello", "world", "pole")
Output: ("l", "o")
Example 4
Input: @words = ("abc", "def", "ghi")
Output: ()
Example 5
Input: @words = ("aab", "aac", "aaa")
Output: ("a", "a")

I did not think it could be possible but Raku can solve this challenge in one line.

And even in this one line, a lot of the code is only for formatting the output in the style of the spec. The important parts start in the middle with @*ARGS which contains the input words in the form of command-line arguments. We use .map() to process each of these. First each argument is converted to a Seq with .comb() and then into a Bag with .Bag(). The keys of this Bag will be the unique letters in that word and the values the number of occurrences of that letter in the word. Then we use [∩] metaoperator to get the intersection of these Bags. The keys of the resulting Set will be only the letters that occur in every word. Some of these letters may occur more than once in every word (such as l in example 1 for instance.) We account for this with another .map(). This time we duplicate each letter (as I mentioned, a .key() in each Bag) with the number of times it occurs (the value() of that key) using the xx operator. The rest of the code is, as I said, for cosmetic purposes.

say q{(}, ([∩] @*ARGS.map({.comb.Bag})).map({ "\"{.key}\"" xx .value }).sort.join(q{, }),q{)}

(Full code on Github.)

Perl doesn't have an intersection operator. I had written a replacement subroutine before but it was not very good so I decided to rewrite it properly.

The subroutine below takes references to two arrays as its' parameters.

sub intersection($arr1, $arr2) {

A Hash is set up that will hold the elements that intersect between the two arrays as keys and the number of times each element occurs as values.

    my %count;

It is populated with the elements of the first array.

    for my $elem (@$arr1) {
        $count{$elem}++; 
    }

Now we create an empty array to hold the intersection.

    my @result;

We then iterate over each element in the second array. For each element...

    for my $elem (@$arr2) {

...we check if that element exists in %count with a non-zero value, which means it was present in the first array and hasn't already been added to the result the maximum number of times it appears in both arrays.

        if ($count{$elem}) {

If so, we push the element into @result and decrement its count in %count to avoid adding duplicates beyond their occurrence in the first array.

            push @result, $elem;
            $count{$elem}--;
        }
    }

Finally, we return the @result array, which contains the intersection of the two input arrays, including duplicate elements if they appear multiple times in both arrays.

    return @result;
}

Our intersection() subroutine still has one drawback over Raku's version; it only works on two arrays at a time. We get around this by first removing the first word from @words with shift() and turning it into an array of individual letters with split().

my @letters = split //, shift @words;

This array is successively combined with each subsequent word and run through intersection() creating a cumulative intersection of, eventually, all the words in the input.

for my $word (@words) {
    @letters = intersection(\@letters, [split //, $word]);
}

This intersection is then printed out in the same style as in the spec.

say q{(}, (join q{, }, map { "\"$_\"" } sort @letters ), q{)};

(Full code on Github.)

Challenge 2:

Find Winner

You are given an array of all moves by the two players.

Write a script to find the winner of the TicTacToe game if found based on the moves provided in the given array.

UPDATE: Order move is in the order - A, B, A, B, A, ….

Example 1
Input: @moves = ([0,0],[2,0],[1,1],[2,1],[2,2])
Output: A

Game Board:
[ A _ _ ]
[ B A B ]
[ _ _ A ]
Example 2
Input: @moves = ([0,0],[1,1],[0,1],[0,2],[1,0],[2,0])
Output: B

Game Board:
[ A A B ]
[ A B _ ]
[ B _ _ ]
Example 3
Input: @moves = ([0,0],[1,1],[2,0],[1,0],[1,2],[2,1],[0,1],[0,2],[2,2])
Output: Draw

Game Board:
[ A A B ]
[ B B A ]
[ A B A ]
Example 4
Input: @moves = ([0,0],[1,1])
Output: Pending

Game Board:
[ A _ _ ]
[ _ B _ ]
[ _ _ _ ]
Example 5
Input: @moves = ([1,1],[0,0],[2,2],[0,1],[1,0],[0,2])
Output: B

Game Board:
[ B B B ]
[ A A _ ]
[ _ _ A ]

I think my solution to this is quite ingenious if I do say so myself.

First we set up some variables to hold state.

$A and $B will hold the cumulative board positions for players A and B respectively.

my $A = 0;
my $B = 0;

The $result hold the current victory state of the game. It is initialized to pending to indicate a game still in progress.

my $result = 'Pending';

The input is taken from the command-line arguments and stored in @moves. For example 1, it would look like this: 0,0 2,0 1,1 2,1 2,2. (Note there's a mistake in the picture that accompanies example 1. It makes no difference to the result though.)

using .kv() we get the index of each argument (stored in $turn) and it's value (stored in $square) $square is again split() into $row and $column and these are converted from strings to integers.

for @moves.kv -> $turn, $square {
    my ($row, $col) = $square.split(q{,})».Int;

If the $turn is an even number it is player A's turn.

    if $turn %% 2 {

This is the ingenious bit. Each square of the board is assigned a unique number like this:

  10 |   11 |   12
-----+------+------
 100 |  101 |  102
-----+------+------
1000 | 1001 | 1002

This values can easily be calculated on the basis of $row and $col like this:

        $A += 10 ** ($row + 1) + $col;

As you will have noticed this number is added to the value of $A.

The value of $A is used to determine if player A is the winner . If so, the $result is changed to A, and we stop processing moves.

        if isWinner($A) {
            $result = 'A';
            last;
        }
    }

isWinner() takes the value of a player ($A or $B) and compares it to a value equal to the sum of the values of either a row column or diagonal. So we can quickly calculate a winning situation without multiple comparisons. It will return True if a winning condition was matched or False otherwise.

sub isWinner($player) {
    return (33, 303, 3003, 1110, 1113, 1116).grep($player).so;
}

Back to MAIN(), if the value of $turn is odd, we follow the same procedure but for player B.

    else {
        $B += 10 ** ($row + 1) + $col;
        if isWinner($B) {
            $result = 'B';
            last;
        }
    }

If we are on the 9th turn and neither side has won, it is a draw. We set the result appropriately and stop processing.

    if $turn == 8 {
        $result = 'Draw';
        last;
    }
}

Finally, we print $result.

say $result;

(Full code on Github.)

The Perl version is a close copy of Raku.

sub isWinner($player) {
    return grep { $_ == $player } (33, 303, 3003, 1110, 1113, 1116);
}

my $A = 0;
my $B = 0;
my $result = 'Pending';

for my $turn (keys @moves) {
    my ($row, $col) = split q{,}, $moves[$turn];

    if ($turn % 2) {
        $B += 10 ** ($row + 1) + $col;
        if (isWinner($B)) {
            $result = 'B';
            last;
        }
    }

    else {
        $A += 10 ** ($row + 1) + $col;
        if (isWinner($A)) {
            $result = 'A';
            last;
        }
    }

    if ($turn == 8) {
        $result = 'Draw';
        last;
    }
}

say $result;

(Full code on Github.)