Perl Weekly Challenge: Exact Change and Array Loops

It’s time for the Perl Weekly Challenge 236!


Task 1: Exact Change

You are asked to sell juice each costs $5. You are given an array of bills. You can only sell ONE juice to each customer but make sure you return exact change back. You only have $5, $10 and $20 notes. You do not have any change in hand at first.

Write a script to find out if it is possible to sell to each customers with correct change.

Example 1

Input: @bills = (5, 5, 5, 10, 20)
Output: true

From the first 3 customers, we collect three $5 bills in order.
From the fourth customer, we collect a $10 bill and give back a $5.
From the fifth customer, we give a $10 bill and a $5 bill.
Since all customers got correct change, we output true.

Example 2

Input: @bills = (5, 5, 10, 10, 20)
Output: false

From the first two customers in order, we collect two $5 bills.
For the next two customers in order, we collect a $10 bill and give back a $5 bill.
For the last customer, we can not give the change of $15 back because we only have two $10 bills.
Since not every customer received the correct change, the answer is false.

Example 3

Input: @bills = (5, 5, 5, 20)
Output: true

Ok, how to attack this problem. I’m sure there’s a clever way to figure out whether or not the given assortment of bills can produce exact change, but as always, I’m going for straightforward and easy to understand over clever. A clever solution would be worth it if we needed performance.

So, the straightforward way is to keep a count of bills we take in and then return whether we can produce exact change. If at any point we don’t have the exact change on hand for a transaction, we can bail out of the function early and return false. If we get through all the transactions and are able to produce exact change for all of them, we return true. I’m going to use a hash because we’re tracking three bill amounts separated by a bit of space, and keeping those in a numerically indexed array would yield a bunch of empty elements I’d need to deal with.

sub isExactChangePossible {
  my @bills = @_;
  my %till; # we keep the bills in a "till"
  BILLS: foreach my $collected ( @bills ) {
    # put the bill we collected in the "till"
    $till{$collected}++;

    # calculate the required change
    my $change_required = $collected - 5;

    # if we don't need to make change,
    # skip to the next bill collected!
    next BILLS unless $change_required;

    # loop through the bills we have on hand
    # in descending size (try to make change
    # with the largest bills possible)
    foreach my $bill ( reverse sort { $a <=> $b } keys %till ) {

      # as long as we have more of this bill and
      # using it would not yield TOO MUCH change
      while ($till{$bill} > 0 && $change_required - $bill >= 0) {
        # deduct the amount from the required change
        $change_required -= $bill;

        # remove the bill from the till
        $till{$bill}--;
      }

      # move on if we managed to make exact change!
      next BILLS unless $change_required;
    }

    # if we weren't able to make change, fail
    return 0 if $change_required;
  }

  # we successfully made change for all transactions!
  return 1;
}

I’m just going to link to the full Perl script in GitHub.


The Raku version is almost identical:

sub isExactChangePossible(*@bills where ($_.all ~~ Int)) {
  my %till; # we keep the bills in a "till"
  BILLS: for @bills -> $collected {
    # put the bill we collected in the "till"
    %till{$collected}++;

    # calculate the required change
    my $change_required = $collected - 5;

    # if we don't need to make change,
    # skip to the next bill collected!
    next BILLS unless $change_required;

    # loop through the bills we have on hand
    for %till.keys().sort({ .Int }).reverse() -> $bill {
      # as long as we have more of this bill and
      # using it would not yield TOO MUCH change
      while (%till{$bill} > 0 && $change_required - $bill >= 0) {
        # deduct the amount from the required change
        $change_required -= $bill;

        # remove the bill from the till
        %till{$bill}--;
      }

      # move on if we managed to make exact change!
      next BILLS unless $change_required;
    }

    # if we weren't able to make change, fail
    return 0 if $change_required;
  }
  
  # we successfully made change for all transactions!
  return 1;
}

The one thing to note is that we can just say that the items being sorted are .Int and Raku will handle the comparison. Here’s the full Raku script in GitHub.


For Python, I had to tweak my logic a little to get around not being able to continue to the next iteration of the outer for bills loop from within the inner for till loop.

def isExactChangePossible(bills):
    till = {}; # we keep the bills in a "till"
    for collected in bills:
        # put the bill we collected in the "till"
        #
        # using .get(collected, 0) yields the value in the
        # dict for the key 'collected' if it exists, or the
        # specified default (in this case, 0) if it doesn't
        till[collected] = till.get(collected, 0) + 1

        # calculate the required change
        change_required = collected - 5

        # loop through the bills we have on hand
        for bill in sorted(till, reverse=True):
            # as long as we have more of this bill and
            # using it would not yield TOO MUCH change
            while till[bill] > 0 and change_required - bill >= 0:
                # deduct the amount from the required change
                change_required -= bill

                # remove the bill from the till
                till[bill] -= 1

        # if we weren't able to make change, fail
        if change_required:
            return 0
  
    # we successfully made change for all transactions!
    return 1

Here’s the full Python script in GitHub.


And now to the Java version. It’s slightly more annoying because Java Maps aren’t native to the language, but the approach works well:

  public static boolean isExactChangePossible(int[] bills) {
    // we keep the bills in a "till"
    HashMap<Integer, Integer> till =
      new HashMap<Integer, Integer>();

    for (int collected : bills) {
      // put the bill we collected in the "till"
      //
      // using .getOrDefault(collected, 0) yields the value
      // in the map for the key 'collected' if it exists, or
      // the specified default (in this case, 0) if it doesn't
      till.put(
        collected,
        till.getOrDefault(collected, 0) + 1
      );

      // calculate the required change
      int change_required = collected - 5;

      // loop through the bills we have on hand, making sure
      // we go from largest to smallest bill
      List<Integer> keys = new ArrayList<>(till.keySet());
      Collections.sort(keys, Collections.reverseOrder());
      for (Integer bill : keys) {
        // as long as we have more of this bill and
        // using it would not yield TOO MUCH change
        while (till.get(bill) > 0 &&
               change_required - bill >= 0) {
          // deduct the amount from the required change
          change_required -= bill;

          // remove the bill from the till
          till.put(bill, till.get(bill) - 1);
        }
      }
      // if we weren't able to make change, fail
      if (change_required > 0) {
          return false;
      }
    }
    return true;
  }

Here’s the full Java script in GitHub.


Task 2: Array Loops

You are given an array of unique integers.

Write a script to determine how many loops are in the given array.

To determine a loop: Start at an index and take the number at array[index] and then proceed to that index and continue this until you end up at the starting index.

Example 1

Input: @ints = (4,6,3,8,15,0,13,18,7,16,14,19,17,5,11,1,12,2,9,10)
Output: 3

To determine the 1st loop, start at index 0, the number at that index is 4, proceed to index 4, the number at that index is 15, proceed to index 15 and so on until you're back at index 0.

Loops are as below:
[4 15 1 6 13 5 0]
[3 8 7 18 9 16 12 17 2]
[14 11 19 10]

Example 2

Input: @ints = (0,1,13,7,6,8,10,11,2,14,16,4,12,9,17,5,3,18,15,19)
Output: 6

Loops are as below:
[0]
[1]
[13 9 14 17 18 15 5 8 2]
[7 11 4 6 10 16 3]
[12]
[19]

Example 3

Input: @ints = (9,8,3,11,5,7,13,19,12,4,14,10,18,2,16,1,0,15,6,17)
Output: 1

Loop is as below:
[9 4 5 7 19 17 15 1 8 12 18 6 13 2 3 11 10 14 16 0]

So to attack this we want to loop over each item in @ints and see if a loop starts at that element. Things I thought about:

  • I was worried about loops that don’t go back to the start, but one of the assumptions is that the list is unique integers, so we’re never going to have to worry about that.
  • Double counting loops: if we start at an element that’s in a loop we’ve already counted, we shouldn’t count it again.
  • When I wanted to pass multiple types of arguments to loopExistsAt(), I decided it would be a good opportunity to use the idea of having named parameters for a Per sub by passing it a hash.
sub loopExistsAt {
  my %params = @_;
  my $ints  = $params{ints};
  my $start = $params{start};
  my $seen  = $params{seen};

  # bail early if we're in a loop we've seen before
  return if exists $seen->{$start};

  my @loop;
  my $i = $start;
  for (;;) {
    # keep track of the values in the order we visit them
    push @loop, $ints->[$i];

    # track where we've already been
    # to avoid double-counting loops
    $seen->{$i} = 1;

    # get the next index
    $i = $ints->[$i];

    # make sure the index is in bounds
    last unless $i >= 0 && $i <= $#{$ints};

    # make sure we haven't seen the index before
    last if exists $seen->{$i};
  }

  # if the last element points back to
  # the start, it's a loop!
  if ($loop[-1] == $start) {
    return @loop;
  }
  # otherwise, return an empty list
  return;
}

sub identifyLoops {
  my @ints = @_;
  my @loops;
  my %seen; # keep track of indices we've seen
            # to avoid duplicating loops
  foreach my $start ( 0 .. $#ints ) {
    my @loop = loopExistsAt(
      start => $start,
      ints  => \@ints,
      seen  => \%seen
    );
    if (@loop) {
      push @loops, \@loop;
    }
  }
  return @loops;
}

Here’s the full Perl script in GitHub.


The Raku version wound up catching on bits of my Raku-newbie knowledge:

  • When I attempted to return nothing with just return;, what I wound up returning was an Any object. If I want to return an empty list, I need to return [];
  • I had to look up how to do named parameters in Raku.
sub loopExistsAt(:@ints, :$start, :%seen) {
  # bail early if we're in a loop we've seen before
  return [] if %seen{$start}:exists;

  my @loop;
  my $i = $start;
  loop (;;) {
    # keep track of the values in the order we visit them
    push @loop, @ints[$i];

    # track where we've already been
    # to avoid double-counting loops
    %seen{$i} = 1;

    # get the next index
    $i = @ints[$i];

    # make sure the index is in bounds
    last unless $i >= 0 && $i < @ints.elems;

    # make sure we haven't seen the index before
    last if %seen{$i}:exists;
  }

  # if the last element points back to
  # the start, it's a loop!
  if (@loop[*-1] == $start) {
    return @loop;
  }
  # otherwise, return an empty list
  return [];
}

sub identifyLoops {
  my @ints = @_;
  my @loops;
  my %seen; # keep track of indices we've seen
            # to avoid duplicating loops
  for 0 .. $@ints.elems - 1 -> $start {
    my @loop = loopExistsAt(
      start => $start,
      ints  => @ints,
      seen  => %seen
    );
    if (@loop) { 
      push @loops, @loop;
    }
  }
  return @loops;
}

Here’s the full Raku script in GitHub.


Python:

def loopExistsAt(ints=[], seen={}, start=0):
    # bail early if we're in a loop we've seen before
    if start in seen:
        return []

    loop = [] # initialize an empty list to start
    i = start # initialize i to starting point
    while True:
        # keep track of the values in the order we visit them
        loop.append(ints[i])

        # track where we've already been
        # to avoid double-counting loops
        seen[i] = 1

        # get the next index
        i = ints[i]

        # make sure the index is in bounds
        if i < 0 or i >= len(ints):
            break

        # make sure we haven't seen the index before
        if i in seen:
            break

    # if the last element points back to
    # the start, it's a loop!
    if loop[-1] == start:
        return loop

    # otherwise, return an empty list
    return []

def identifyLoops(ints):
    loops = []
    seen = {}; # keep track of indices we've seen
               # to avoid duplicating loops
    for start in range(0, len(ints)):
        loop = loopExistsAt(
          start = start,
          ints  = ints,
          seen  = seen
        )
        if loop:
            loops.append(loop)
    return loops

Here’s the full Python script in GitHub.


Java:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.stream.Collectors;

public class Ch2 {
  public static ArrayList<Integer> loopExistsAt(
    int start, int[] ints, HashMap<Integer, Integer> seen
  ) {
    // bail early if we're in a loop we've seen before
    if (seen.get(start) != null) {
      // return an empty ArrayList
      return new ArrayList<Integer>();
    }

    // initialize an empty list to start
    ArrayList<Integer> loop = new ArrayList<Integer>();
    // initialize i to starting point
    int i = start;
    while (true) {
      // keep track of the values in the order we visit them
      loop.add(ints[i]);

      // track where we've already been
      // to avoid double-counting loops
      seen.put(i, 1);

      // get the next index
      i = ints[i];

      // make sure the index is in bounds
      if (i < 0 || i >= ints.length) {
        break;
      }

      // make sure we haven't seen the index before
      if (seen.get(i) != null) {
        break;
      }
    }

    // if the last element points back to
    // the start, it's a loop!
    if (loop.get(loop.size() - 1) == start) {
        return loop;
    }

    // otherwise, return an empty ArrayList
    return new ArrayList<Integer>();
  }

  public static ArrayList<ArrayList<Integer>> identifyLoops(int[] ints) {
    ArrayList<ArrayList<Integer>> loops =
      new ArrayList<ArrayList<Integer>>();
    HashMap<Integer, Integer> seen = 
      new HashMap<Integer, Integer>();

    for (int i = 0; i < ints.length; i++) {
      ArrayList<Integer> loop = loopExistsAt(i, ints, seen);
      if (loop.size() > 0) {
        loops.add(loop);
      }
    }
    return loops;
  }

  public static String comma_joined(int[] ints) {
    // we're using it more than once, make it a method
    return Arrays.stream(ints)
                 .mapToObj(String::valueOf)
                 .collect(Collectors.joining(","));
  }

  public static void solution(int[] ints) {
    System.out.println("Input: @ints = (" + comma_joined(ints) +
                       ")");
    ArrayList<ArrayList<Integer>> loops = identifyLoops(ints);
    int count = loops.size();
    System.out.println(String.format("Output: %1$d", count));
    if (count > 0) {
      String loop_noun = (count == 1) ? "Loop" : "Loops";
      String are_verb  = (count == 1) ? "is"   : "are";
      System.out.println("\n" + loop_noun + " " + are_verb +
                         " as below:");

      for (ArrayList<Integer> loop : loops) {
        String as_list = loop.stream()
                             .map(String::valueOf)
                             .collect(Collectors.joining(" "));
        System.out.println("[" + as_list + "]");
      }
    }
  }

  public static void main(String[] args) {
    System.out.println("Example 1:");
    solution(new int[] {4,6,3,8,15,0,13,18,7,16,14,
                        19,17,5,11,1,12,2,9,10});

    System.out.println("\nExample 2:");
    solution(new int[] {0,1,13,7,6,8,10,11,2,14,16,
                        4,12,9,17,5,3,18,15,19});

    System.out.println("\nExample 3:");
    solution(new int[] {9,8,3,11,5,7,13,19,12,4,14,
                        10,18,2,16,1,0,15,6,17});
  }
}

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