This is my response to The Weekly Challenge #234.
Input: @words = ("java", "javascript", "julia")
Output: ("j", "a")
Example 2:
Input: @words = ("bella", "label", "roller")
Output: ("e", "l", "l")
Example 3:
Input: @words = ("cool", "lock", "cook")
Output: ("c", "o")
Coercing each word into a Set (with
.Set
), thus removing duplicates, and taking the intersection
of those sets (with the Intersection Operator ∩
or the ascii
version (&)
) gives the common characters:
> say "java".comb.Set (&) "javascript".comb.Set (&) "julia".comb.Set
Set(a j)
See
docs.raku.org/type/Set
for more information about the Set
type.
See docs.raku.org/routine/(&), infix ∩ for more information about the intersection operator.
We can actually skip the .Set
calls, as the intersection operater
does the right thing anyway.
> say "java".comb (&) "javascript".comb (&) "julia".comb
Set(a j)
File: common-characters-set
#! /usr/bin/env raku
unit sub MAIN (*@words where @words.elems > 0 # [1]
&& all(@words) ~~ /^<[a..z]>+$/, # [1a]
:v(:$verbose));
my @sets = @words>>.comb>>.Set; # [2]
my @common = ( [(&)] @sets ).keys; # [3]
say ":Sets: { @sets.raku }" if $verbose;
say "({ @common.map( '"' ~ * ~ '"').join(", ") })"; # [4]
[1] Ensure at least one element, and they must consist of english lower case letters only; and at least one letter.
[2] Get the sets. We can skip this call, as shown above, but the verbose output uses the sets.
[3] Get the intersection. The result is s set, so we apply keys
to get
the characters.
[4] Print the common characters.
Running it:
$ ./common-characters-set java javascript julia
("j", "a")
$ ./common-characters-set bella label roller
("l", "e")
$ ./common-characters-set cool lock cook
("c", "o")
The second one is wrong, but let us do verbose mode before explaining why:
$ ./common-characters-set -v java javascript julia
:Sets: [Set.new("v","j","a"),
Set.new("j","i","t","c","v","r","p","s","a"),
Set.new("i","a","u","j","l")]
("j", "a")
$ ./common-characters-set -v bella label roller
:Sets: [Set.new("l","e","a","b"),
Set.new("e","a","l","b"),
Set.new("l","e","r","o")]
("l", "e")
$ ./common-characters-set -v cool lock cook
:Sets: [Set.new("o","c","l"),
Set.new("l","o","k","c"),
Set.new("o","c","k")]
("o", "c")
The problem is the duplicates, as the Set
does not handle them.
But we can use the Bag
type instead, as it has a concept of
weight.
#! /usr/bin/env raku
unit sub MAIN (*@words where @words.elems > 0
&& all(@words) ~~ /^<[a..z]>+$/,
:v(:$verbose));
my @bags = @words>>.comb>>.Bag; # [1]
my @common = ( [(&)] @bags ).kxxv; # [2]
say ":Bags: { @bags.raku }" if $verbose;
say "({ @common.map( '"' ~ * ~ '"').join(", ") })";
[1] Turn the words into the bag type this time.
[2] We have to keep the weight, and the kxxv
operator does just that (gives a list with the keys added as many times as
their weight implies).
See
docs.raku.org/routine/kxxv
for more information about kxxv
.
Note that I mentioned
kxxv
in Words of a Sort,
the weekly challenge from last week. Then I could not actually use it, but
this week it pans out. Fancy that...
$ ./common-characters-bag -v java javascript julia
:Bags: [("v"=>1,"j"=>1,"a"=>2).Bag,
("t"=>1,"a"=>2,"r"=>1,"v"=>1,"j"=>1,"p"=>1,"s"=>1,"c"=>1,"i"=>1).Bag,
("i"=>1,"j"=>1,"l"=>1,"a"=>1,"u"=>1).Bag]
("a", "j")
$ ./common-characters-bag -v bella label roller
:Bags: [("e"=>1,"b"=>1,"a"=>1,"l"=>2).Bag,
("b"=>1,"a"=>1,"l"=>2,"e"=>1).Bag,
("l"=>2,"r"=>2,"e"=>1,"o"=>1).Bag]
("e", "l", "l")
$ ./common-characters-bag -v cool lock cook
:Bags: [("o"=>2,"l"=>1,"c"=>1).Bag,
("o"=>1,"l"=>1,"k"=>1,"c"=>1).Bag,
("k"=>1,"c"=>1,"o"=>2).Bag]
("o", "c")
Looking good...
Except the order of the characters in the last one. Hash like structures does not have a concept of order, so running the same command a second time will give a different random order:
$ ./common-characters-bag -v cool lock cook
:Bags: [("c"=>1,"o"=>2,"l"=>1).Bag,
("o"=>1,"c"=>1,"l"=>1,"k"=>1).Bag,
("o"=>2,"c"=>1,"k"=>1).Bag]
("c", "o")
Looking a the examples shows that the output order is given by the first word.
File: common-characters
#! /usr/bin/env raku
unit sub MAIN (*@words where @words.elems > 0
&& all(@words) ~~ /^<[a..z]>+$/,
:v(:$verbose));
my @bags = @words>>.comb>>.Bag;
my @common = ( [(&)] @bags ).kxxv;
say ":Bags: { @bags.raku }" if $verbose;
my $common = @common.BagHash; # [1]
my @sorted; # [2]
for @words[0].comb -> $letter # [3]
{
say ": Considering first word letter $letter" if $verbose;
next unless $common{$letter}; # [4]
$common{$letter}--; # [5]
@sorted.push: $letter; # [6]
say ": - Added letter $letter (remaining count: $common{$letter})"
if $verbose;
}
say "({ @sorted.map( '"' ~ * ~ '"').join(", ") })"; # [7]
[1] Start with the common letters and give us a BagHash
,
a mutable (writeable) version of a Bag
- as we are going to
change it later on (in [5]).
[2] The common letters will end up here, in the correct sorted order.
[3] Iterate over the letters in the first word, giving the correct sorting order.
[4] Skip the letter if it does not appear in the BagHash.
[5] We have used one instance of the letter; there may be more.
[6] Add the letter to the result.
[7] Print the correctly sorted result.
Running it:
$ ./common-characters -v java javascript julia
:Bags: [("j"=>1,"v"=>1,"a"=>2).Bag, ("j"=>1,"p"=>1,"c"=>1,"i"=>1,"a"=>2,"t"=>1,"v"=>1,"r"=>1,"s"=>1).Bag, ("l"=>1,"a"=>1,"u"=>1,"j"=>1,"i"=>1).Bag]
: Considering first word letter j
: - Added letter j (remaining count: 0)
: Considering first word letter a
: - Added letter a (remaining count: 0)
: Considering first word letter v
: Considering first word letter a
("j", "a")
$ ./common-characters -v bella label roller
:Bags: [("l"=>2,"b"=>1,"e"=>1,"a"=>1).Bag, ("b"=>1,"a"=>1,"l"=>2,"e"=>1).Bag, ("l"=>2,"e"=>1,"r"=>2,"o"=>1).Bag]
: Considering first word letter b
: Considering first word letter e
: - Added letter e (remaining count: 0)
: Considering first word letter l
: - Added letter l (remaining count: 1)
: Considering first word letter l
: - Added letter l (remaining count: 0)
: Considering first word letter a
("e", "l", "l")
$ ./common-characters -v cool lock cook
:Bags: [("l"=>1,"c"=>1,"o"=>2).Bag, ("c"=>1,"o"=>1,"l"=>1,"k"=>1).Bag, ("o"=>2,"c"=>1,"k"=>1).Bag]
: Considering first word letter c
: - Added letter c (remaining count: 0)
: Considering first word letter o
: - Added letter o (remaining count: 0)
: Considering first word letter o
: Considering first word letter l
("c", "o")
(i, j, k)
that
satisfies num[i] != num[j]
, num[j] != num[k]
and
num[k] != num[i]
.
Input: @ints = (4, 4, 2, 4, 3)
Ouput: 3
(0, 2, 4) because 4 != 2 != 3
(1, 2, 4) because 4 != 2 != 3
(2, 3, 4) because 2 != 4 != 3
Example 2:
Input: @ints = (1, 1, 1, 1, 1)
Ouput: 0
Example 3:
Input: @ints = (4, 7, 1, 10, 7, 4, 1, 1)
Output: 28
triplets of 1, 4, 7 = 3x2×2 = 12 combinations
triplets of 1, 4, 10 = 3×2×1 = 6 combinations
triplets of 4, 7, 10 = 2×2×1 = 4 combinations
triplets of 1, 7, 10 = 3x2x1 = 6 combinations
#! /usr/bin/env raku
unit sub MAIN (*@ints where all(@ints) ~~ Int && all(@ints) > 0, # [1]
:v(:$verbose));
my $bag = @ints.Bag; # [2]
my $combinations = 0; # [3]
if $bag.keys >= 3 # [4]
{
my @unique = @ints.unique.sort; # [5]
for @unique.combinations(3) -> @combination # [6]
{
my $add = [*] @combination.map({ $bag{$_} }); # [7]
say ": Triplet: ({ @combination.join(",") }) has $add combinations"
if $verbose;
$combinations += $add; # [8]
}
}
say $combinations; # [9]
[1] An array consisting of positive integers only. Note that an empty array is allowed.
[2] Turn the input into a Bag.
[3] The total number of combinations will end up here.
[4] Only compute the total if we have three or more unique values. If not, the answer will be zero.
[5] Get the unique values, in sorted order (so that we will get the same
sorted result each time we run the program). Note that we could have
used the Bag instead; $bag.keys.sort
.
[6] Iterate over all the possible 3-element combinations of the unique
values (with combinations
).
[7] Each positive integer in the
combination is replaced with the weight (with map
), and we
multiply them all together with the Reduction Metaoperator []
.
[8] Add that sum from [7] to the total.
[9] Print the result (the total number of unique combinations).
Running it:
$ ./unequal-triplets 4 4 2 4 3
3
$ ./unequal-triplets 1 1 1 1 1
0
$ ./unequal-triplets 4 7 1 10 7 4 1 1
28
Looking good.
With verbose mode:
$ ./unequal-triplets -v 4 4 2 4 3
: Triplet: (2,3,4) has 3 combinations
3
$ ./unequal-triplets -v 1 1 1 1 1
0
$ ./unequal-triplets -v 4 7 1 10 7 4 1 1
: Triplet: (1,4,7) has 12 combinations
: Triplet: (1,4,10) has 6 combinations
: Triplet: (1,7,10) has 6 combinations
: Triplet: (4,7,10) has 4 combinations
28
And that's it.