Perl Weekly Challenge: “They call me the count, because I love to count pairs! Ah, ah, ah!”

Count Common sounds entirely too much like a title, and to me there’s only one person who holds the title of Count… “Ah, my happy childhood in the Carpathian mountains… how well I remember!”

Now let us count common words and find strong pairs for Perl Weekly Challenge 277!

Task 1: Count Common

You are given two array of strings, @words1 and @words2.

Write a script to return the count of words that appears in both arrays exactly once.

Example 1

Input: @words1 = ("Perl", "is", "my", "friend")
       @words2 = ("Perl", "and", "Raku", "are", "friend")
Output: 2

The words "Perl" and "friend" appear once in each array.

Example 2

Input: @words1 = ("Perl", "and", "Python", "are", "very", "similar")
       @words2 = ("Python", "is", "top", "in", "guest", "languages")
Output: 1

Example 3

Input: @words1 = ("Perl", "is", "imperative", "Lisp", "is",
                  "functional")
       @words2 = ("Crystal", "is", "similar", "to", "Ruby")
Output: 0

Approach

We’re counting how often things appear, so again, it makes sense to use a Multiset/Bag. We make a bag for each word list, eliminate elements with a multiplicity of greater than 1, and then look for elements that are in both multisets.

Raku

In Raku, we have the Bag type, and we have the intersection operator (∩) to make this easy.

sub countCommon(@words1, @words2) {
  # make Bags that count the words
  my %counts1 = @words1.map({ $_.lc }).Bag;
  my %counts2 = @words2.map({ $_.lc }).Bag;

  # filter to get set of words occuring only once
  my @counts1 = %counts1.keys.grep({ %counts1{$_} == 1});
  my @counts2 = %counts2.keys.grep({ %counts2{$_} == 1});

  # find the elements common in both
  my @common = @counts1@counts2;
  return @common.elems;
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
Example 1:
Input: @words1 = ("Perl", "is", "my", "friend")
       @words2 = ("Perl", "and", "Raku", "are", "friend")
Output: 2

Example 2:
Input: @words1 = ("Perl", "and", "Python", "are", "very", "similar")
       @words2 = ("Python", "is", "top", "in", "guest", "languages")
Output: 1

Example 3:
Input: @words1 = ("Perl", "is", "imperative", "Lisp", "is", "functional")
       @words2 = ("Crystal", "is", "similar", "to", "Ruby")
Output: 0

Perl

In Perl, it’s really easy to use hashes to make a multiset/bag. I mean, there’s a Set::Bag package in CPAN, but it doesn’t have a constructor that can just take an array the way Raku’s Bag can, so it’s just as easy to roll our own.

Getting the intersection of two arrays, however, is enough work that just using Set::Intersection from CPAN is worth it, considering it makes finding the intersection just a single function call away.

use Set::Intersection;

sub countCommon($words1, $words2) {
  # make Bags that count the words
  my %counts1; map { $counts1{lc $_}++ } @$words1;
  my %counts2; map { $counts2{lc $_}++ } @$words2;

  # filter to get set of words occuring only once
  my @counts1 = grep { $counts1{$_} == 1 } keys %counts1;
  my @counts2 = grep { $counts2{$_} == 1 } keys %counts2;

  # find the elements common in both
  my @common = get_intersection(\@counts1, \@counts2);
  return scalar(@common);
}

View the entire Perl script for this task on GitHub.

Python

Again, we’re using the Counter type in the collections module as our Bag, and we’re using a set to make finding the intersection easy.

from collections import Counter

def countCommon(words1, words2):
  # make Counters that count the words
  counts1 = Counter([ w.lower() for w in words1 ])
  counts2 = Counter([ w.lower() for w in words2 ])

  # filter to get set of words occuring only once
  counts1 = set([ w for w in counts1.keys() if counts1[w] == 1 ])
  counts2 = set([ w for w in counts2.keys() if counts2[w] == 1 ])

  # find the elements common in both
  common = counts1 & counts2
  return len(common)

View the entire Python script for this task on GitHub.

Elixir

Like last time, I’m borrowing code from the external module Multiset to handle the multiset operations. Everything else can be handled easily with built-in types.

  def countCommon(words1, words2) do
    # make Multisets that count the words
    counts1 = words1
      |> Enum.map(fn w -> String.downcase(w) end)
      |> Multiset.new
    counts2 = words2
      |> Enum.map(fn w -> String.downcase(w) end)
      |> Multiset.new

    # filter to get set of words occuring only once
    counts1 = counts1
      |> Multiset.values
      |> Enum.filter(fn w -> Multiset.multiplicity(counts1, w) == 1 end)
      |> MapSet.new
    counts2 = counts2
      |> Multiset.values
      |> Enum.filter(fn w -> Multiset.multiplicity(counts2, w) == 1 end)
      |> MapSet.new

    # find the elements common in both
    common = MapSet.intersection(counts1, counts2)
    MapSet.size(common)
  end

View the entire Elixir script for this task on GitHub.


Task 2: Strong Pair

You are given an array of integers, @ints.

Write a script to return the count of all strong pairs in the given array.

A pair of integers x and y is called strong pair if it satisfies: 0 < |x – y| < min(x, y).

Example 1

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

Strong Pairs: (2, 3), (3, 4), (3, 5), (4, 5)

Example 2

Input: @ints = (5, 7, 1, 7)
Output: 1

Strong Pairs: (5, 7)

Approach

Ok, this problem needs some parsing. It says “array of integers”, but the examples are all positive integers. Also, the second example has the integer 7 appear in the input list twice, but it only appears in the sample output strong pairs once. So, I’m making the assumption that each strong pair is unique regardless of the ordering of the pair. This explains why the output for example 2 isn’t (5, 7), (5, 7) because 7 appeared in the input list twice.

Because of this, I’m sorting the input list and making it unique, and I’m discarding any pairings where x is larger than y (because we’ll already have compared that pair when x was the smaller of the pair). Also, making a function isStrongPair just allows the code to be more readable.

Raku

One of the things that I’d nearly forgotten was that Raku allows chaining of relational operators!

sub isStrongPair($x, $y) {
  return 0 < abs($x - $y) < min($x, $y);
}

sub strongPairs(@ints) {
  my @strong;
  my @ints2 = @ints.unique.sort;
  for @ints2 -> $x {
    for @ints2 -> $y {
      next if $x > $y;
      next unless isStrongPair($x, $y);
      @strong.push("($x, $y)");
    }
  }
  return @strong.elems, @strong.join(', ');
}

View the entire Raku script for this task on GitHub.

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

Strong Pairs: (2, 3), (3, 4), (3, 5), (4, 5)

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

Strong Pairs: (5, 7)

Perl

In Perl, we need List::Util‘s min and uniq functions.

use List::Util qw( min uniq );

sub isStrongPair($x, $y) {
  return 0 < abs($x - $y) < min($x, $y);
}

sub strongPairs(@ints) {
  my @strong;
  my @ints2 = uniq sort @ints;
  foreach my $x ( @ints2 ) {
    foreach my $y ( @ints2 ) {
      next if $x > $y;
      next unless isStrongPair($x, $y);
      push @strong, "($x, $y)";
    }
  }
  return scalar(@strong), join(', ', @strong);
}

View the entire Perl script for this task on GitHub.

Python

def isStrongPair(x, y):
  return( 0 < abs(x - y) < min(x, y) )

def strongPairs(ints):
  strong = []
  ints = set(sorted(ints))
  for x in ints:
    for y in ints:
      if x < y and isStrongPair(x, y):
        strong.append( f'({x}, {y})' )
  return len(strong), ', '.join(strong)

View the entire Python script for this task on GitHub.

Elixir

In the Elixir solution, I don’t need to do the x < y comparison because I’m pulling integers off the top of the list so they won’t be processed multiple times.

  def isStrongPair(x, y) do
    diff = abs(x - y)
    0 < diff and diff < min(x, y)
  end

  def findStrongPairs([], strong), do: strong

  def findStrongPairs([x | rest], strong) do
    strongY = Enum.filter(rest, fn y -> isStrongPair(x, y) end)
    strong = strong ++ Enum.map(strongY, fn y -> "(#{x}, #{y})" end)
    findStrongPairs(rest, strong)
  end

  def strongPairs(ints) do
    ints = ints
    |> Enum.uniq
    |> Enum.sort
    strong = findStrongPairs(ints, [])
    { length(strong), Enum.join(strong, ", ") }
  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-277/packy-anderson