Perl Weekly Challenge: Exact Change Only!

Nothing jumped out at me for the theme this week, so I started free-associating. The first task, “No Connection”, makes me think of bus routes, and that’s reinforced by the second task, “Making Change”. But I’ve already used The Hollies Bus Stop, so I started thinking about busses, and how they keep getting stuck in traffic, and I landed on Jimi Hendrix’s Crosstown Traffic.

So, with the musical background set, let’s not slow down and get to the other side of Perl Weekly Challenge 285.

Task 1: No Connection

You are given a list of routes, @routes.

Write a script to find the destination with no further outgoing connection.

Example 1

Input: @routes = (["B","C"], ["D","B"], ["C","A"])
Output: "A"

"D" -> "B" -> "C" -> "A".
"B" -> "C" -> "A".
"C" -> "A".
"A".

Example 2

Input: @routes = (["A","Z"])
Output: "Z"

Approach

Ok, I don’t like the description of this task one bit. We’re given a list of routes, but it doesn’t explain what the routes are. Considering it’s a list of two-element arrays (or “tuples”), and the first example shows what I am assuming are trips that all start with the first element of these tuples, I am presuming that the each element in the list is a tuple of [starting point, ending point] for a “trip”. Then there’s the phrase “find the destination”. In both of the examples, there exists only one ending point that is not the starting point for some other trip, but none of this is explicitly stated in the task description, so I will once again presume that this is a requirement.

So, the easiest way to find the one ending point for a trip that is not a starting point for another trip is to create two sets/lists: one of starting points, one of ending points, and find elements in the second set that aren’t in the first.

Raku

In Raku, it’s easy to use the Set class to get us the ability to find the set difference (elements in one set that aren’t in another)

sub noConnection(@routes) {
  my $starts = set @routes.map({ .[0] });
  my $ends   = set @routes.map({ .[1] });
  my $noConnection = $ends (-) $starts;
  return 'no terminal destinations'
    if $noConnection.elems == 0;
  return 'multiple terminal destinations'
    if $noConnection.elems > 1;
  return $noConnection;
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
Example 1:
Input: @routes = ([B, C], [D, B], [C, A])
Output: A

Example 2:
Input: @routes = ([A, Z])
Output: Z

Bad Input: multiple terminal destinations
Input: @routes = ([A, B], [C, D])
Output: multiple terminal destinations

Bad Input: no terminal destinations
Input: @routes = ([A, Z], [Z, A])
Output: no terminal destinations

Perl

Perl, however, doesn’t have a built-in set type, so I found the Set::Scalar module on CPAN.

use Set::Scalar;

sub noConnection(@routes) {
  my $starts = Set::Scalar->new(map { $_->[0] } @routes);
  my $ends   = Set::Scalar->new(map { $_->[1] } @routes);
  my $noConnection = $ends - $starts;
  return 'no terminal destinations'
    if $noConnection->is_empty;
  return 'multiple terminal destinations'
    if $noConnection->size > 1;
  return ($noConnection->elements)[0];
}

View the entire Perl script for this task on GitHub.

Python

Sets are built in to Python, but because sets are supposed to be iterated over, it isn’t easy to get an individual element. I got around this by converting the set back to a list so I could get the single element in it by itself, but I also could have done next(iter(noConnection)).

def noConnection(routes):
  starts = set([ x[0] for x in routes ])
  ends   = set([ x[1] for x in routes ])
  noConnection = ends - starts
  if len(noConnection) == 0:
    return 'no terminal destinations'
  if len(noConnection) > 1:
    return 'multiple terminal destinations'
  return list(noConnection)[0]

View the entire Python script for this task on GitHub.

Elixir

MapSet is the module for manipulating set in Elixir.

  def noConnection(routes) do
    starts = routes
    |> Enum.map(fn x -> List.first(x) end)
    |> MapSet.new
    ends   = routes
    |> Enum.map(fn x -> List.last(x) end)
    |> MapSet.new
    noConnection = MapSet.difference(ends, starts)
    cond do
      MapSet.size(noConnection) == 0 ->
        "no terminal destinations"
      MapSet.size(noConnection) > 1 ->
        "multiple terminal destinations"
      true ->
        noConnection |> MapSet.to_list |> List.first
    end
  end

View the entire Elixir script for this task on GitHub.


Task 2: Making Change

Compute the number of ways to make change for given amount in cents. By using the coins e.g. PennyNickelDimeQuarter and Half-dollar, in how many distinct ways can the total value equal to the given amount? Order of coin selection does not matter.

A penny (P) is equal to 1 cent.
A nickel (N) is equal to 5 cents.
A dime (D) is equal to 10 cents.
A quarter (Q) is equal to 25 cents.
A half-dollar (HD) is equal to 50 cents.

Example 1

Input: $amount = 9
Output: 2

1: 9P
2: N + 4P

Example 2

Input: $amount = 15
Output: 6

1: D + 5P
2: D + N
3: 3N
4: 2N + 5P
5: N + 10P
6: 15P

Example 3

Input: $amount = 100
Output: 292

Approach

Initially, I was going to do this entire thing with nested loops and hashes holding the counts of each kind of coin… but it got unwieldy pretty quick. That’s when I realized that I could use recursion to offload a bunch of my work: figure out how many of the largest possible coins I could use, subtract their value from the total amount, and then use recursion to figure out all the combinations of coins for the remaining amount. Since I would be recursively calling my makeChange function repeatedly with the same values, so to improve performance, I want to cache the output of makeChange.

Raku

For some reason, the is cached trait is still experimental, so we need to enable it with use experimental :cached.

use experimental :cached;

# establish the values of the coins
my %types = 50 => 'HD', 25 => 'Q', 10 => 'D', 5 => 'N', 1 => 'P';

# make a sorted list of coin values
my @values = %types.keys.sort(*.Int);

# handle formatting coins to not display the number 1
sub formatCoin($coin, $count) {
  return ( ($count == 1 ?? "" !! $count) ~ $coin );
}

sub makeChange($amount, $exclude = 0) is cached {
  my @output;
  for @values.reverse -> $value {
    # if this type of coin is worth more
    # than the total, we can't use it
    next if $value > $amount;

    # if we're excluding this coin value or greater, skip it
    next if $exclude && $value >= $exclude;

    my $coin = %types{$value}; # get the coin name

    if ($value == 1) {
      # pennies are easy!
      @output.push( formatCoin($coin, $amount) );
    }
    else {
      # loop from max number of this coin we can use down to 1
      for Int($amount / $value) ... 1 -> $count {
        # figure out how much change we need after we've used
        # $count many of coin $coin
        my $left = $amount - ($count * $value);

        if ($left > 0) { # we need more change
          # make a recursive call to get the combinations
          # for that remaining amount, excluding any coins
          # of equal or greater value to $value
          my @combinations = makeChange($left, $value);
          for @combinations -> $combo {
            my $this_coin = formatCoin($coin, $count);
            @output.push([$this_coin, $combo].join(" + "));
          }
        }
        else { # this is exact change!
          @output.push( formatCoin($coin, $count) );
        }
      }
    }
  }

  return @output;
}

View the entire Raku script for this task on GitHub.

$ raku/ch-2.raku
Example 1:
Input: $amount = 9
Output: 2

1: N + 4P
2: 9P

Example 2:
Input: $amount = 15
Output: 6

1: D + N
2: D + 5P
3: 3N
4: 2N + 5P
5: N + 10P
6: 15P

Example 3:
Input: $amount = 100
Output: 292

1: 2HD
2: HD + 2Q
3: HD + Q + 2D + N
4: HD + Q + 2D + 5P
5: HD + Q + D + 3N
6: HD + Q + D + 2N + 5P
7: HD + Q + D + N + 10P
8: HD + Q + D + 15P
9: HD + Q + 5N
10: HD + Q + 4N + 5P
11: HD + Q + 3N + 10P
12: HD + Q + 2N + 15P
13: HD + Q + N + 20P
14: HD + Q + 25P
15: HD + 5D
16: HD + 4D + 2N
17: HD + 4D + N + 5P
18: HD + 4D + 10P
19: HD + 3D + 4N
20: HD + 3D + 3N + 5P
21: HD + 3D + 2N + 10P
22: HD + 3D + N + 15P
23: HD + 3D + 20P
24: HD + 2D + 6N
25: HD + 2D + 5N + 5P
26: HD + 2D + 4N + 10P
27: HD + 2D + 3N + 15P
28: HD + 2D + 2N + 20P
29: HD + 2D + N + 25P
30: HD + 2D + 30P
31: HD + D + 8N
32: HD + D + 7N + 5P
33: HD + D + 6N + 10P
34: HD + D + 5N + 15P
35: HD + D + 4N + 20P
36: HD + D + 3N + 25P
37: HD + D + 2N + 30P
38: HD + D + N + 35P
39: HD + D + 40P
40: HD + 10N
41: HD + 9N + 5P
42: HD + 8N + 10P
43: HD + 7N + 15P
44: HD + 6N + 20P
45: HD + 5N + 25P
46: HD + 4N + 30P
47: HD + 3N + 35P
48: HD + 2N + 40P
49: HD + N + 45P
50: HD + 50P
51: 4Q
52: 3Q + 2D + N
53: 3Q + 2D + 5P
54: 3Q + D + 3N
55: 3Q + D + 2N + 5P
56: 3Q + D + N + 10P
57: 3Q + D + 15P
58: 3Q + 5N
59: 3Q + 4N + 5P
60: 3Q + 3N + 10P
61: 3Q + 2N + 15P
62: 3Q + N + 20P
63: 3Q + 25P
64: 2Q + 5D
65: 2Q + 4D + 2N
66: 2Q + 4D + N + 5P
67: 2Q + 4D + 10P
68: 2Q + 3D + 4N
69: 2Q + 3D + 3N + 5P
70: 2Q + 3D + 2N + 10P
71: 2Q + 3D + N + 15P
72: 2Q + 3D + 20P
73: 2Q + 2D + 6N
74: 2Q + 2D + 5N + 5P
75: 2Q + 2D + 4N + 10P
76: 2Q + 2D + 3N + 15P
77: 2Q + 2D + 2N + 20P
78: 2Q + 2D + N + 25P
79: 2Q + 2D + 30P
80: 2Q + D + 8N
81: 2Q + D + 7N + 5P
82: 2Q + D + 6N + 10P
83: 2Q + D + 5N + 15P
84: 2Q + D + 4N + 20P
85: 2Q + D + 3N + 25P
86: 2Q + D + 2N + 30P
87: 2Q + D + N + 35P
88: 2Q + D + 40P
89: 2Q + 10N
90: 2Q + 9N + 5P
91: 2Q + 8N + 10P
92: 2Q + 7N + 15P
93: 2Q + 6N + 20P
94: 2Q + 5N + 25P
95: 2Q + 4N + 30P
96: 2Q + 3N + 35P
97: 2Q + 2N + 40P
98: 2Q + N + 45P
99: 2Q + 50P
100: Q + 7D + N
101: Q + 7D + 5P
102: Q + 6D + 3N
103: Q + 6D + 2N + 5P
104: Q + 6D + N + 10P
105: Q + 6D + 15P
106: Q + 5D + 5N
107: Q + 5D + 4N + 5P
108: Q + 5D + 3N + 10P
109: Q + 5D + 2N + 15P
110: Q + 5D + N + 20P
111: Q + 5D + 25P
112: Q + 4D + 7N
113: Q + 4D + 6N + 5P
114: Q + 4D + 5N + 10P
115: Q + 4D + 4N + 15P
116: Q + 4D + 3N + 20P
117: Q + 4D + 2N + 25P
118: Q + 4D + N + 30P
119: Q + 4D + 35P
120: Q + 3D + 9N
121: Q + 3D + 8N + 5P
122: Q + 3D + 7N + 10P
123: Q + 3D + 6N + 15P
124: Q + 3D + 5N + 20P
125: Q + 3D + 4N + 25P
126: Q + 3D + 3N + 30P
127: Q + 3D + 2N + 35P
128: Q + 3D + N + 40P
129: Q + 3D + 45P
130: Q + 2D + 11N
131: Q + 2D + 10N + 5P
132: Q + 2D + 9N + 10P
133: Q + 2D + 8N + 15P
134: Q + 2D + 7N + 20P
135: Q + 2D + 6N + 25P
136: Q + 2D + 5N + 30P
137: Q + 2D + 4N + 35P
138: Q + 2D + 3N + 40P
139: Q + 2D + 2N + 45P
140: Q + 2D + N + 50P
141: Q + 2D + 55P
142: Q + D + 13N
143: Q + D + 12N + 5P
144: Q + D + 11N + 10P
145: Q + D + 10N + 15P
146: Q + D + 9N + 20P
147: Q + D + 8N + 25P
148: Q + D + 7N + 30P
149: Q + D + 6N + 35P
150: Q + D + 5N + 40P
151: Q + D + 4N + 45P
152: Q + D + 3N + 50P
153: Q + D + 2N + 55P
154: Q + D + N + 60P
155: Q + D + 65P
156: Q + 15N
157: Q + 14N + 5P
158: Q + 13N + 10P
159: Q + 12N + 15P
160: Q + 11N + 20P
161: Q + 10N + 25P
162: Q + 9N + 30P
163: Q + 8N + 35P
164: Q + 7N + 40P
165: Q + 6N + 45P
166: Q + 5N + 50P
167: Q + 4N + 55P
168: Q + 3N + 60P
169: Q + 2N + 65P
170: Q + N + 70P
171: Q + 75P
172: 10D
173: 9D + 2N
174: 9D + N + 5P
175: 9D + 10P
176: 8D + 4N
177: 8D + 3N + 5P
178: 8D + 2N + 10P
179: 8D + N + 15P
180: 8D + 20P
181: 7D + 6N
182: 7D + 5N + 5P
183: 7D + 4N + 10P
184: 7D + 3N + 15P
185: 7D + 2N + 20P
186: 7D + N + 25P
187: 7D + 30P
188: 6D + 8N
189: 6D + 7N + 5P
190: 6D + 6N + 10P
191: 6D + 5N + 15P
192: 6D + 4N + 20P
193: 6D + 3N + 25P
194: 6D + 2N + 30P
195: 6D + N + 35P
196: 6D + 40P
197: 5D + 10N
198: 5D + 9N + 5P
199: 5D + 8N + 10P
200: 5D + 7N + 15P
201: 5D + 6N + 20P
202: 5D + 5N + 25P
203: 5D + 4N + 30P
204: 5D + 3N + 35P
205: 5D + 2N + 40P
206: 5D + N + 45P
207: 5D + 50P
208: 4D + 12N
209: 4D + 11N + 5P
210: 4D + 10N + 10P
211: 4D + 9N + 15P
212: 4D + 8N + 20P
213: 4D + 7N + 25P
214: 4D + 6N + 30P
215: 4D + 5N + 35P
216: 4D + 4N + 40P
217: 4D + 3N + 45P
218: 4D + 2N + 50P
219: 4D + N + 55P
220: 4D + 60P
221: 3D + 14N
222: 3D + 13N + 5P
223: 3D + 12N + 10P
224: 3D + 11N + 15P
225: 3D + 10N + 20P
226: 3D + 9N + 25P
227: 3D + 8N + 30P
228: 3D + 7N + 35P
229: 3D + 6N + 40P
230: 3D + 5N + 45P
231: 3D + 4N + 50P
232: 3D + 3N + 55P
233: 3D + 2N + 60P
234: 3D + N + 65P
235: 3D + 70P
236: 2D + 16N
237: 2D + 15N + 5P
238: 2D + 14N + 10P
239: 2D + 13N + 15P
240: 2D + 12N + 20P
241: 2D + 11N + 25P
242: 2D + 10N + 30P
243: 2D + 9N + 35P
244: 2D + 8N + 40P
245: 2D + 7N + 45P
246: 2D + 6N + 50P
247: 2D + 5N + 55P
248: 2D + 4N + 60P
249: 2D + 3N + 65P
250: 2D + 2N + 70P
251: 2D + N + 75P
252: 2D + 80P
253: D + 18N
254: D + 17N + 5P
255: D + 16N + 10P
256: D + 15N + 15P
257: D + 14N + 20P
258: D + 13N + 25P
259: D + 12N + 30P
260: D + 11N + 35P
261: D + 10N + 40P
262: D + 9N + 45P
263: D + 8N + 50P
264: D + 7N + 55P
265: D + 6N + 60P
266: D + 5N + 65P
267: D + 4N + 70P
268: D + 3N + 75P
269: D + 2N + 80P
270: D + N + 85P
271: D + 90P
272: 20N
273: 19N + 5P
274: 18N + 10P
275: 17N + 15P
276: 16N + 20P
277: 15N + 25P
278: 14N + 30P
279: 13N + 35P
280: 12N + 40P
281: 11N + 45P
282: 10N + 50P
283: 9N + 55P
284: 8N + 60P
285: 7N + 65P
286: 6N + 70P
287: 5N + 75P
288: 4N + 80P
289: 3N + 85P
290: 2N + 90P
291: N + 95P
292: 100P

Perl

In Perl, caching the makeChange function is accomplished through the Memoize module. I thought that Perl’s .. operator allowed you to count backwards, but I tried it and found out it didn’t, and I found an old PerlMonks post about why. Fortunately, reverse is optimized to take 1 .. N and just reverse it.

use Memoize;
memoize('makeChange');

# establish the values of the coins
my %types = (50 => 'HD', 25 => 'Q', 10 => 'D', 5 => 'N', 1 => 'P');

# make a sorted list of coin values
my @values = sort { $a <=> $b } keys %types;

# handle formatting coins to not display the number 1
sub formatCoin($coin, $count) {
  return ( ($count == 1 ? "" : $count) . $coin );
}

sub makeChange($amount, $exclude = 0) {
  my @output;
  foreach my $value (reverse @values) {
    # if this type of coin is worth more
    # than the total, we can't use it
    next if $value > $amount;

    # if we're excluding this coin value or greater, skip it
    next if $exclude && $value >= $exclude;

    my $coin = $types{$value}; # get the coin name

    if ($value == 1) {
      # pennies are easy!
      push @output, formatCoin($coin, $amount);
    }
    else {
      # loop from max number of this coin we can use down to 1
      foreach my $count ( reverse 1 .. int($amount / $value) ) {
        # figure out how much change we need after we've used
        # $count many of coin $coin
        my $left = $amount - ($count * $value);

        if ($left > 0) { # we need more change
          # make a recursive call to get the combinations
          # for that remaining amount, excluding any coins
          # of equal or greater value to $value
          my @combinations = makeChange($left, $value);
          foreach my $combo ( @combinations ) {
            my $this_coin = formatCoin($coin, $count);
            push @output, join(" + ", $this_coin, $combo);
          }
        }
        else { # this is exact change!
          push @output, formatCoin($coin, $count);
        }
      }
    }
  }

  return @output;
}

View the entire Perl script for this task on GitHub.

Python

Caching in Python comes from @functools.cache.

from functools import cache

# establish the values of the coins
types = { 50: 'HD', 25: 'Q', 10: 'D', 5: 'N', 1: 'P' }

# make a sorted list of coin values
values = sorted(types.keys(), reverse=True)

def formatCoin(coin, count):
  return f"{count}{coin}" if count > 1 else coin
          
@cache
def makeChange(amount, exclude = 0):
  output = []
  for value in values:
    # if this type of coin is worth more
    # than the total, we can't use it
    if value > amount:
      continue

    # if we're excluding this coin value or greater, skip it
    if exclude and value >= exclude:
      continue

    coin = types[value] # get the coin name

    if value == 1:
      # pennies are easy!
      output.append( formatCoin(coin, amount) )
    else:
      # loop from max number of this coin we can use down to 1
      for count in range(int(amount / value), 0, -1):
        # figure out how much change we need after we've used
        # $count many of coin $coin
        left = amount - (count * value)

        if left > 0: # we need more change
          # make a recursive call to get the combinations
          # for that remaining amount, excluding any coins
          # of equal or greater value to $value
          combinations = makeChange(left, value)
          for combo in combinations:
            this_coin = formatCoin(coin, count)
            output.append(" + ".join([this_coin, combo]))
        else: # this is exact change!
          output.append( formatCoin(coin, count) )
  return output

View the entire Python script for this task on GitHub.

Elixir

I figured I was ahead with my Elixir solution because I was already using recursion, but I wound up having to break it out into a lot of smaller functions because of Elixir’s functional nature. One of the things I did was put guards on versions of makeChange/4 (lines 41-42) to handle when the value of a coin is larger than the remaining amount or if we’re excluding this coin from this recursive call (lines 22 & 25 of the Raku solution), and (lines 49-50) when we’re just dealing with pennies (lines 29-32 of the Raku solution).

  # establish the values of the coins
  @coin_types %{ 50 => "HD", 25 => "Q", 10 => "D", 5 => "N", 1 => "P" }

  # make a sorted list of coin values
  @coin_values @coin_types |> Map.keys |> Enum.sort |> Enum.reverse

  def formatCoin(coin, count) do
    if count > 1 do
      "#{to_string(count)}#{coin}"
    else
      coin
    end
  end

  def joinCoins(x, y) do
    Enum.join([ x, y ], " + ")
  end

  def addCoins(count, coin, amount, _, output) when amount == 0 do
    output ++ [ formatCoin(coin, count) ]
  end

  def addCoins(count, coin, amount, value, output) do
    # make a recursive call to get the combinations
    # for that remaining amount, excluding any coins
    # of equal or greater value to $value
    {_, output} = makeChange(amount, value)
    |> Enum.map_reduce(output, fn combo, output ->
      this_coin = formatCoin(coin, count)
      {
        combo,
        output ++ [ joinCoins(this_coin, combo) ]
      }
    end)
    output
  end

  def makeChange([value | remaining], amount, exclude, output)
      when (value > amount) or (exclude > 0 and value >= exclude) do
    # if this type of coin is worth more than the total,
    # we can't use it OR
    # if we're excluding this coin value or greater, skip it
    makeChange(remaining, amount, exclude, output)
  end

  def makeChange([value], amount, _, output)
      when value == 1 do
    # we're adding pennies
    output ++ [ formatCoin("P", amount) ]
  end

  def makeChange([value | remaining], amount, exclude, output) do
    coin = @coin_types |> Map.get(value) # get the coin name
    # loop from max number of this coin we can use down to 1
    {_, output} = 1 .. Integer.floor_div(amount, value)
    |> Enum.reverse
    |> Enum.map_reduce(output,
      fn count, output2 ->
        # figure out how much change we need after we've used
        # $count many of coin $coin
        left = amount - (count * value)
        { count, addCoins(count, coin, left, value, output2) }
      end)
    makeChange(remaining, amount, exclude, output)
  end

  def makeChange(amount, exclude \\ 0) do
    makeChange(@coin_values, amount, exclude, [])
  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-285/packy-anderson