Perl Weekly Challenge: Comparatively Distinct, but Great^H^H^H^H^HAverage

They say that “comparison is the thief of joy”, but the people who say that aren’t computer scientists.

I’ve only got one song on my music server at home that has “compare” in the title, so please enjoy it (and have a link to the artists while you’re at it).

So now let’s head on into this week’s distinctly unique Perl Weekly Challenge 321.

Task 1: Distinct Average

You are given an array of numbers with even length.

Write a script to return the count of distinct average. The average is calculated by removing the minimum and the maximum, then average of the two.

Example 1

Input: @nums = (1, 2, 4, 3, 5, 6)
Output: 1

Step 1: Min = 1, Max = 6, Avg = 3.5
Step 2: Min = 2, Max = 5, Avg = 3.5
Step 3: Min = 3, Max = 4, Avg = 3.5

The count of distinct average is 1.

Example 2

Input: @nums = (0, 2, 4, 8, 3, 5)
Output: 2

Step 1: Min = 0, Max = 8, Avg = 4
Step 2: Min = 2, Max = 5, Avg = 3.5
Step 3: Min = 3, Max = 4, Avg = 3.5

The count of distinct average is 2.

Example 3

Input: @nums = (7, 3, 1, 0, 5, 9)
Output: 2

Step 1: Min = 0, Max = 9, Avg = 4.5
Step 2: Min = 1, Max = 7, Avg = 4
Step 3: Min = 3, Max = 5, Avg = 4

The count of distinct average is 2.

Approach

I have to admit, it took me a LONG TIME to figure out what the “count of distinct average” was asking me to do. It was pretty clear that I needed to loop over the list, pulling off the min and max values and calculating the average for them, but what I didn’t understand was how those averages related to the number being spit out at the end; how did a list of three 3.5 values produce the number 1? Why was (4, 3.5, 3.5) yielding 2?

Then I saw it: it was counting the number of distinct values in the list of averages.

Even before I figured out what this was asking for, most of it was obvious: I need to sort the list, and then loop until the list is empty. On each loop, I pull out the first and last elements of the list, average them, and then put them into a list of averages. Then I determine how many distinct values this list of averages has.

Raku

Two weeks ago I wanted to use a Bag and wound up not doing so, and it just occurred to me that a Bag would be perfect for determining how many distinct values there are because a bag only stores each key once.

sub distinctAverages(@nums) {
  @nums = @nums.sort;
  my ($explain, $step) = (q{}, 0);
  my %averages = Bag.new;
  while (@nums) {
    my ($min, $max) = (@nums.shift, @nums.pop);
    my $avg = ($min + $max) / 2;
    $step++;
    $explain ~= "Step $step: Min = $min, "
             ~  "Max = $max, Avg = $avg\n";
    %averages{$avg}++;
  }
  $explain ~= "\nThe count of distinct average is "
           ~  %averages.elems ~ ".";
  return %averages.elems, $explain;
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
Example 1:
Input: @nums = (1, 2, 4, 3, 5, 6)
Output: 1

Step 1: Min = 1, Max = 6, Avg = 3.5
Step 2: Min = 2, Max = 5, Avg = 3.5
Step 3: Min = 3, Max = 4, Avg = 3.5

The count of distinct average is 1.

Example 2:
Input: @nums = (0, 2, 4, 8, 3, 5)
Output: 2

Step 1: Min = 0, Max = 8, Avg = 4
Step 2: Min = 2, Max = 5, Avg = 3.5
Step 3: Min = 3, Max = 4, Avg = 3.5

The count of distinct average is 2.

Example 3:
Input: @nums = (7, 3, 1, 0, 5, 9)
Output: 2

Step 1: Min = 0, Max = 9, Avg = 4.5
Step 2: Min = 1, Max = 7, Avg = 4
Step 3: Min = 3, Max = 5, Avg = 4

The count of distinct average is 2.

Example 4:
Input: @nums = (1, 9, 2, 6, 3, 4)
Output: 3

Step 1: Min = 1, Max = 9, Avg = 5
Step 2: Min = 2, Max = 6, Avg = 4
Step 3: Min = 3, Max = 4, Avg = 3.5

The count of distinct average is 3.

Perl

Sadly, Perl doesn’t have Bags, but a hash works just as well. Evaluating keys %hash in a scalar context will give the number of keys in the hash. All in all, the Perl solution looks a lot like the Raku solution (probably because my Raku looks like Perl).

sub distinctAverages(@nums) {
  @nums = sort @nums;
  my ($explain, $step) = (q{}, 0);
  my %averages =();
  while (@nums) {
    my ($min, $max) = (shift(@nums), pop(@nums));
    my $avg = ($min + $max) / 2;
    $step++;
    $explain .= "Step $step: Min = $min, "
             .  "Max = $max, Avg = $avg\n";
    $averages{$avg}++;
  }
  my $count = scalar(keys %averages);
  $explain .= "\nThe count of distinct average is $count.";
  return $count, $explain;
}

View the entire Perl script for this task on GitHub.

Python

I keep saying the Counter datatype in the collections module is essentially the same as a Bag.

from collections import Counter

def distinctAverages(nums):
  nums.sort() # list.sort() works in place
  explain, step = "", 0
  averages = Counter()
  while nums:
    min, max = nums.pop(0), nums.pop(-1)
    avg = (min + max) / 2
    # since in Python, this will always produce a real number,
    # we need to do a little more work to make sure avg is an
    # integer when it has no fractional component
    if avg == int(avg):
      avg = int(avg)
    step += 1
    explain += (
      f"Step {step}: Min = {min}, Max = {max}, Avg = {avg}\n"
    )
    averages[avg] += 1
  count = len(list(averages))
  explain += (
    f"\nThe count of distinct average is {count}."
  )
  return count, explain

View the entire Python script for this task on GitHub.

Elixir

As usual, in Elixir we need to use recursion to do the heavy lifting a while loop would do in other languages. The first definition of distinctAverages defines the stopping condition when the list is empty, the second definition of distinctAverages does the work of popping off the min/max and calculating the average and stashing that in a Map so we can count the number of distinct averages we get. The final definition of distinctAverages is the one we initially call with just the list of integers, and it sorts the list and counts the keys in the averages map returned by the recursive call to the second distinctAverages.

  def distinctAverages([], step, explain, averages),
    do: {explain, averages}

  def distinctAverages(nums, step, explain, averages) do
    {min, nums} = List.pop_at(nums, 0)
    {max, nums} = List.pop_at(nums, -1)
    avg = (min + max) / 2
    # since in Elixir, this will always produce a float,
    # we need to do a little more work to make sure avg is an
    # integer when it has no fractional component
    avg = cond do
      avg == trunc(avg) -> trunc(avg)
      true              -> avg
    end
    explain = explain <> "Step #{step}: Min = #{min}, "
    explain = explain <> "Max = #{max}, Avg = #{avg}\n"
    averages = averages |> Map.put(avg, 1)
    distinctAverages(nums, step+1, explain, averages)
  end

  def distinctAverages(nums) do
    {explain, averages} =
        distinctAverages(Enum.sort(nums), 1, "", %{})
    count = averages |> Map.keys |> length
    {
      count,
      explain <> "\nThe count of distinct average is #{count}."
    }
  end

View the entire Elixir script for this task on GitHub.


Task 2: Backspace Compare

You are given two strings containing zero or more #.

Write a script to return true if the two given strings are same by treating # as backspace.

Example 1

Input: $str1 = "ab#c"
       $str2 = "ad#c"
Output: true

For first string,  we remove "b" as it is followed by "#".
For second string, we remove "d" as it is followed by "#".
In the end both strings became the same.

Example 2

Input: $str1 = "ab##"
       $str2 = "a#b#"
Output: true

Example 3

Input: $str1 = "a#b"
       $str2 = "c"
Output: false

Approach

This problem screams regular expressions to me. We match any character followed by a #, and remove both the character and the #, and repeat until we run out of #.

Raku

At first, I did the replacement as a single s:g/.\#// per string, but I discovered that only making a single pass with a global replace on ab## yielded a# (because in the one pass, it only matched the b followed by the #; once we removed the b#, the pointer to the remainder of the string only had the single #, so it didn’t match. I realized that a single s/.\#// match run repeatedly until it stopped matching anything would produce the result I wanted.

sub backspaceCompare($str1, $str2) {
  # if we pass in these as 'is rw', it expects to be passed
  # a writable container, which a string literal is not.
  # it's just easier to copy it to a writable container.
  my $s1 = $str1; while ($s1 ~~ s/.\#//) {}
  my $s2 = $str2; while ($s2 ~~ s/.\#//) {}
  return $s1 eq $s2;
} 

View the entire Raku script for this task on GitHub.

$ raku/ch-2.raku
Example 1:
Input: $str1 = "ab#c"
       $str2 = "ad#c"
Output: True

Example 2:
Input: $str1 = "ab##"
       $str2 = "a#b#"
Output: True

Example 3:
Input: $str1 = "a#b"
       $str2 = "c"
Output: False

Perl

Perl worked almost exactly the same, except I didn’t need to worry about the parameters being writable.

sub backspaceCompare($str1, $str2) {
  while ($str1 =~ s/.\#//) {}
  while ($str2 =~ s/.\#//) {}
  return $str1 eq $str2 ? 'true' : 'false';
}

View the entire Perl script for this task on GitHub.

Python

Python proved to be slightly trickier; since re.sub doesn’t return whether or not the substitution was successful, it returns the modified string. So I needed to save the result in a new variable, and then compare that to the original value. If it was different, replace the original variable with the new variable, and then repeat.

import re

def backspaceCompare(str1, str2):
  s1 = re.sub(r'.\#', "", str1)
  while s1 != str1:
    str1 = s1
    s1 = re.sub(r'.\#', "", str1)
  s2 = re.sub(r'.\#', "", str2)
  while s2 != str2:
    str2 = s2
    s2 = re.sub(r'.\#', "", str2)
  return s1 == s2

View the entire Python script for this task on GitHub.

Elixir

Elixir has the same problem as Python in that Regex.replace/4 returns the resulting string, but defining a little recursive private function allowed me to handle that easily.

  defp repeatReplace(str) do
    s = Regex.replace(~r/.\#/, str, "")
    cond do
      s != str -> repeatReplace(s)
      true     -> s
    end
  end

  def backspaceCompare(str1, str2) do
    repeatReplace(str1) == repeatReplace(str2)
  end

View the entire Elixir script for this task on GitHub.


Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-321/packy-anderson

Leave a Reply