With Raku to
Mum's Platform

by Arne Sommer

With Raku to Mum's Platform

[144] Published 5. September 2021.

This is my response to the Perl Weekly Challenge #128.

Challenge #128.1: Maximum Sub-Matrix

You are given m x n binary matrix having 0 or 1.

Write a script to find out maximum sub-matrix having only 0.

Example 1:
Input : [ 1 0 0 0 1 0 ]
        [ 1 1 0 0 0 1 ]
        [ 1 0 0 0 0 0 ]

Output: [ 0 0 0 ]
        [ 0 0 0 ]
Example 2:
Input : [ 0 0 1 1 ]
        [ 0 0 0 1 ]
        [ 0 0 1 0 ]

Output: [ 0 0 ]
        [ 0 0 ]
        [ 0 0 ]

I have chosen do solve this task by generating all the possible sub-matrices. It worked for the second part of Four Corners with Raku (Find Square), so it should work equally well here. (The way of specifying the matrix is copied from that article as well.)

File: maximum-sub-matrix
#! /usr/bin/env raku

unit sub MAIN (Str $matrix = "1 0 0 0 1 0 | 1 1 0 0 0 1 | 1 0 0 0 0 0", # [1]
  :v(:$verbose)); 

my @m = $matrix.split("|")>>.words>>.Numeric;                           # [2]

die "Illegal characters" unless all($matrix.words) eq any(0,1,'|');     # [3]

die "No zeros" unless any($matrix.words) eq '0';                        # [4]
die "Uneven row length" unless all(@m>>.elems) == @m[0].elems           # [5]
  && @m[0].elems > 0;

my $number-of-rows = @m.elems;     my $last-row = $number-of-rows -1;   # [6]
my $number-of-cols = @m[0].elems;  my $last-col = $number-of-cols -1;   # [7]

say ": Rows: $number-of-rows, cols: $number-of-cols" if $verbose;

my $largest-size = 0;                                                   # [8]
my $largest-rows = 0;                                                   # [9]
my $largest-cols = 0;                                                   # [10]

for 0 .. $last-row -> $from-row                                         # [11]
{
  for $last-row ... $from-row -> $to-row                                # [11a]
  {
    for 0 .. $last-col -> $from-col                                     # [12]
    {
      for $last-col ... $from-col -> $to-col                            # [12a]
      {
        my $size = ($to-row - $from-row +1) * ($to-col - $from-col +1); # [13]
        next if $size <= $largest-size;                                 # [14]

        my @rect =
	  get-rectangle(@m, $from-row, $from-col, $to-row, $to-col);    # [15]
			 
	my $zero = zero-rectangle(@rect);                               # [16]
	
        say ": Checking rectangle [UL: $from-row, $from-col][LR: $to-row, $to-col]: ",
	  "{ @rect.raku }  ---> $zero" if $verbose;

        if $zero                                                        # [17]
	{
	  my $rows = 1 + $to-row - $from-row;                           # [17a]
	  my $cols = 1 + $to-col - $from-col;                           # [17b]
          my $size = $rows * $cols ;                                    # [17c]

          if $size > $largest-size                                      # [18]
	  {
	    say ": - Largest so far $size ($rows x $cols)" if $verbose;
            $largest-size = $size;                                      # [18a]
            $largest-rows = $rows;                                      # [18a]
            $largest-cols = $cols;                                      # [18a]
          }
        }
      }
    }
  }
}

if $largest-size                                                        # [19]
{
  say ": Largest with size $largest-size ($largest-rows x $largest-cols)" if $verbose;
  say "[ { '0' xx $largest-cols } ]" for ^$largest-rows;                # [20]
}
else
{
  say "[]";                                                             # [21]
}

sub get-rectangle(@matrix, $x1, $y1, $x2, $y2)        # [15a]
{
  return (($x1 .. $x2).map({ @m[$_][$y1 .. $y2] }));  # [15b]
}

sub zero-rectangle (@matrix)
{
  return so all(@matrix.List.flat) eq "0";            # [16b]
}

[1] The default matrix, taken from the first example. Specify the values on each row separated by space(s), and a vertical bar between the rows.

[2] Coerce the values to numeric values (so that we can avoid the NumStr pit fall). This will result in a list of list, instead of the default list of sequences. .List.

[3] Ensure that the matrix does not contain anything besides 0 and 1 (and the vertical bar). It is easier to do this on the input string than a matrix, thus the vertical bar.

[4] Exit if the matrix does not contain any zeros at all.

[5] Ensure that the rows have the same length (as the first one), and that the length is non-zero.

[6] Get the number of rows, and the last index.

[7] Ditto, for the columns.

[8] We are looking for the largest sub-matrix. The largest is the one with the highest number of elements. We stroe the current higest one here.

[9] Ditto, for the number of rows. (Note the we do not store the coordinates of the sub-matrix. We do not have to, as we can print zeroes without looking up the sub-matrix.)

[10] Ditto, for the number of columns.

[11] We are going to iterate over all the possible sub-matrices. Start with the largest possible one (the entire matrix), and go smaller. We start with the rows (also [11a]).

[12] Then the columns (also [12b]).

[13] Calculate the size (of the current sub-matrix).

[14] Skip this sub-matrix if the size is smaller than the best we have found so far.

[15] Get the sub-matrix. Note the main matrix as the first parameter [15a], and the clever use of ranges (twice) and map to get the sub-matrix [15b].

[16] Do we have a zero-only matrix? Note the List coercer on the matrix, as the rows are sequences. (As we did not apply a coercer in [15b].)

[17] We have a zero-only sub-matrix. Calculate the number of rows [17a], columns [17b] and the size [17c]

[18] Is this one better than the previous "high score"? Then save the new values [18a]

[19] Do we have a sub-matrix? (Note that [4] makes this redundant.)

[20] • Print the sub-matrix, with the correct number of columns (with the list repetition operator xx) and rows (with a for loop).

[21] An empty sum-matrix.

See docs.raku.org/type/NumStr for more information about the NumStr type.

See docs.raku.org/routine/all for more information about the all Junction.

See docs.raku.org/routine/any for more information about the any Junction.

See docs.raku.org/routine/xx for more information about the list repetition operator xx.

Running it:

$ ./maximum-sub-matrix
[ 0 0 ]
[ 0 0 ]
[ 0 0 ]

$ ./maximum-sub-matrix "1 0 0 0 1 0 | 1 1 0 0 0 1 | 1 0 0 0 0 0"
[ 0 0 ]
[ 0 0 ]
[ 0 0 ]

$ ./maximum-sub-matrix "0 0 1 1 | 0 0 0 1 | 0 0 1 0"
[ 0 0 ]
[ 0 0 ]
[ 0 0 ]

Note that the first example (the two first executions) did not give the same result as the challenge, but the sub-matrix is equally valid (as it has the same size).

We can fix that (if you consider it an error), by shuffling the loops (doing the columns before the rows):

File: maximum-sub-matrix-swapped (changes only)
for 0 .. $last-col -> $from-col
{
  for $last-col ... $from-col -> $to-col
  {
    for 0 .. $last-row -> $from-row
    {
      for $last-row ... $from-row -> $to-row
      {

This version gives us the sub-matrix specified in the challenge:

$ ./maximum-sub-matrix-swapped "1 0 0 0 1 0 | 1 1 0 0 0 1 | 1 0 0 0 0 0"
[ 0 0 0 ]
[ 0 0 0 ]

Some more:

$ ./maximum-sub-matrix-swapped  "1"
No zeros
  in sub MAIN at …

$ ./maximum-sub-matrix-swapped  "0"
[ 0 ]

$ ./maximum-sub-matrix-swapped  "0 | 0"
[ 0 ]
[ 0 ]

$ ./maximum-sub-matrix-swapped  "0 1 | 1 0"
[ 0 ]

Challenge #128.2: Minimum Platforms

You are given two arrays of arrival and departure times of trains at a railway station.

Write a script to find out the minimum number of platforms needed so that no train needs to wait.

Example 1:
Input: @arrivals   = (11:20, 14:30)
       @departures = (11:50, 15:00)
Output: 1

    The 1st arrival of train is at 11:20 and this is the only train at
    the station, so you need 1 platform. Before the second arrival at
    14:30, the first train left the station at 11:50, so you still need
    only 1 platform.
Example 2:
Input: @arrivals   = (10:20, 11:00, 11:10, 12:20, 16:20, 19:00)
       @departures = (10:30, 13:20, 12:40, 12:50, 20:20, 21:20)
Output: 3

    Between 12:20 and 12:40, there would be at least 3 trains at the
    station, so we need minimum 3 platforms.

I'll do this in an object oriented way. The general idea: Set up a station, and add trains to it. The station takes care of the platform allocations. In the end, get the number of tracks from the station.

Here is the program, excluding the essential content of the classes:

File: minimum-platforms (partial)
#! /usr/bin/env raku

subset HHMM where $_ ~~ /^(<[012]>)\d\:<[012345]>\d$/ && $0 <= 23;  # [1]

unit sub MAIN (Str $trains = "11:20 11:50 | 14:30 15:00",    # [2]
   :l(:$loquacious),                                         # [3]
   :v(:$verbose) = $loquacious,                              # [3a]
); 

class Platform { }                                           # [4]

class Station  { }                                           # [5]

my $station = Station.new(name => 'Grand Central');          # [6]

my @trains = $trains.split("|");                             # [7]

for @trains -> $from-to                                      # [8]
{
  my (HHMM $from, HHMM $to) = $from-to.words;                # [9]

  say ": Visiting train $from -> $to" if $verbose;       

  $station.add-train($from, $to);                            # [10]
}

say ": Station: { $station.raku }" if $loquacious;           # [3b]

say $station.number-of-platforms;                            # [11]

[1] We use a custom type, set up with subset to verify the time values.

[2] Default values from the first example. Note the format used, which differ quite a lot from the one specified in the challenge - as I think that this way (pairing the arrival and departure times) is better.

[3] Verbose mode, with another one (loquacious) on top. Note the default value for verbose mode, so that «-l» can be used to enable both.

[4] A class for a single platform.

[5] A class for the station, containing as many platforms as needed (which we will get to in the next code block).

[6] Start the show by setting up a station. The name does not really matter.

[7] Get the trains. This gives us a list of «arrival and departure» strings.

[8] For each train,

[9] • Split the string in two HHMM values. The typed variables will terminate the program if the strings do not satisfy the HHMM rule.

[10] • Add the train to the station object.

[11] Print the number of platforms (that the station has).

See docs.raku.org/language/typesystem#index-entry-subset-subset for more information about subset.

Then the classes, starting with the Platform:

File: minimum-platforms (partial)
class Platform
{
  has Str $.id;                                  # [12]
  has Str @.in-use is rw;                        # [13]

  method add-if-vacant (HHMM $from, HHMM $to)    # [14]
  {
    for @.in-use -> $interval                    # [15]
    {
      my ($start, $end) = $interval.split("-");  # [16]
      next if $to  lt $start;                    # [17]
      next if $end lt $from;                     # [18]

      return False;                              # [19]
    }

    @.in-use.push: "$from-$to";                  # [20]
    return True;                                 # [21]
  }
}

[12] The platform has a name or number. It is not really nedeed in our program, but passengers do like them.

[13] A list of times the platform is in use. The times are on the «HH:MM-HH:MM» form, which is an internal format.

[14] This method adds the train to the platform, if it is vacant in the relevant timespan. It returns True if it was able to add the train, and False if not.

[15] For each train using this platform,

[16] • Get the start and end times, from the internal format (as described in [13]).

[17] If the new train is entirely before the current one, skip it (no collision with the current one). Note the use of string comparison (with lt), as we have strings. Also note the less than comparison, so that a train cannot arrive at the same time as another one is leaving. We are using minutes, so this ensures one a minute byffer between departure and arrival.

[18] If the new train is entirely after the current one, skip it (no collision with the current one).

[19] We have a collision; return False and give in.

[20] No collisions. Add the train,

[21] and return True for success.

And finally, the Station class:

File: minimum-platforms (the remainder)
class Station
{
  has Str      $.name;                                     # [22]
  has Platform @.platforms is rw;                          # [23]

  method add-train (HHMM $from, HHMM $to)                  # [24]
  {
    for self.platforms -> $platform                        # [25]
    {
      return True if $platform.add-if-vacant($from, $to);  # [26]
    }
      
    my $platform = Platform.new(id => (self.number-of-platforms + 1).Str);
                                                           # [27]

    self.platforms.push: $platform;                        # [28]
    return $platform.add-if-vacant($from, $to);            # [29]
  }

  method number-of-platforms                               # [30]
  {
    return @.platforms.elems;                              # [31]
  }
}

[22] The station has a name. They usually do...

[23] And it has a list of platforms (Platform objects). Note the is rw trait, so that we can add to the platform list after setting up the station.

[24] Use this method to add a train to the station.

[25] For each platform,

[26] • return True (i.e. short circuit the loop) if we were able to add the train to that platform.

[27] We were unable to add the train to an existing platform (or there were none), so add a new one. Note the platform ID, a sequential number. Also note the .Str coercer, as the ID field requires a string - and the value is an integer.

[28] Add the new platform to the list of platforms.

[29] Add the train to the newly created platform. This is the first train on that platform, so this call will return True.

[30] Method giving the number of platform for this station,

[31] which is the number of elements in the platform list - and is the answer to the challenge.

Running it:

$ ./minimum-platforms 
1

$ ./minimum-platforms "11:00 12:00 | 12:01 14:00"
1

$ ./minimum-platforms "10:20 10:30 | 11:00 13:20 | 11:10 12:40 | 12:20 \
   12:50 | 16:20 20:20 | 19:00 21:20"
3

Looking good.

With verbose mode:

$ ./minimum-platforms -v "11:00 12:00 | 12:01 14:00"
: Visiting train 11:00 -> 12:00
: Visiting train 12:01 -> 14:00
1

$ ./minimum-platforms -v "10:20 10:30 | 11:00 13:20 | 11:10 12:40 | 12:20 \
  12:50 | 16:20 20:20 | 19:00 21:20"
: Visiting train 10:20 -> 10:30
: Visiting train 11:00 -> 13:20
: Visiting train 11:10 -> 12:40
: Visiting train 12:20 -> 12:50
: Visiting train 16:20 -> 20:20
: Visiting train 19:00 -> 21:20
3

Even more verbose, with loquacious mode:

$ ./minimum-platforms -l "11:00 12:00 | 12:01 14:00"
: Visiting train 11:00 -> 12:00
: Visiting train 12:01 -> 14:00
: Station: Station.new(name => "Grand Central", platforms => Array[Platform].new(Platform.new(id => "1", in-use => Array[Str].new("11:00-12:00", "12:01-14:00"))))
1

$ ./minimum-platforms -l "10:20 10:30 | 11:00 13:20 | 11:10 12:40 | 12:20 12:50 | 16:20 20:20 | 19:00 21:20"
: Visiting train 10:20 -> 10:30
: Visiting train 11:00 -> 13:20
: Visiting train 11:10 -> 12:40
: Visiting train 12:20 -> 12:50
: Visiting train 16:20 -> 20:20
: Visiting train 19:00 -> 21:20
: Station: Station.new(name => "Grand Central", platforms => Array[Platform].new(Platform.new(id => "1", in-use => Array[Str].new("10:20-10:30", "11:00-13:20", "16:20-20:20")), Platform.new(id => "2", in-use => Array[Str].new("11:10-12:40", "19:00-21:20")), Platform.new(id => "3", in-use => Array[Str].new("12:20-12:50"))))
3

Note that the program does not handle trains that arrive after they arrive; as in «arrives before midnight, and departs after it»:

$ ./minimum-platforms -v "10:20 10:30 | 23:30 01:00 | 00:50 00:55"
: Visiting train 10:20 -> 10:30
: Visiting train 23:30 -> 01:00
: Visiting train 00:50 -> 00:55
1

(The second and third trains overlap, requiring different platforms.)

Fixing this requires some refactoring, as we have to split the time period in two. E.g. «23:30-01:00» becomes «23:30-23:59» and «00:00-01:00», and we must add support for adding two periods at once.

A simpler solution is to prohibit it. Let us do just that.

Dumping the Station object gives the internal data structure, which is somewhat lacking in readability. Let us fix that at the same time.

File: minimum-platforms-nicer
#! /usr/bin/env raku

subset HHMM where $_ ~~ /^(<[012]>)\d\:<[012345]>\d$/ && $0 <= 23;

unit sub MAIN (Str $trains = "11:20 11:50 | 14:30 15:00",
   :l(:$loquacious),
   :v(:$verbose) = $loquacious,
); 

class Platform
{
  has Str $.id;
  has Str @.in-use is rw;

  method add-if-vacant (HHMM $from, HHMM $to)
  {
    for @.in-use -> $interval
    {
      my ($start, $end) = $interval.split("-");
      next if $to  lt $start;
      next if $end lt $from;

      return False;
    }

    @.in-use.push: "$from-$to";
    return True;
  }

  method Str                                                  # [1]
  {
    return ":  Platform: $.id: " ~ @.in-use.sort.join(", ");  # [3]
  }
}

class Station
{
  has Str      $.name;
  has Platform @.platforms is rw;

  method add-train (HHMM $from, HHMM $to)
  {
    for self.platforms -> $platform
    {
      return True if $platform.add-if-vacant($from, $to);
    }
      
    my $platform = Platform.new(id => (self.number-of-platforms + 1).Str);
    self.platforms.push: $platform;
    return $platform.add-if-vacant($from, $to);
  }

  method number-of-platforms
  {
    return @.platforms.elems;
  }

  method Str                                                      # [1]
  {
    return ": Station: $.name\n" ~ @.platforms>>.Str.join("\n");  # [2]
  }
}

my $station = Station.new(name => 'Grand Central');

my @trains = $trains.split("|");

for @trains -> $from-to
{
  my (HHMM $from, HHMM $to) = $from-to.words;

  die "Train departs ($from) before it arrives ($to)" if $from gt $to;

  say ": Visiting train $from -> $to" if $verbose;

  $station.add-train($from, $to);
}

say $station.Str if $loquacious;  # [4]

say $station.number-of-platforms;

[1] Printing an object will stringify it for us. This uses a method inherited from the type system, that usually does not give us what we want (as it has no idea of the class content and purpose). But we can supply a custom Str method, as done here.

[2] A Station object stringifies to the station name, followed by the platform stringification on separate lines. @.platforms>>.Str stringifies each object in the list.

[3] A Platform object stringifies to the platform name, followed by the time periods it is in use, orted. The periods are strings, so they are sorted as strings (which works out nicely).

[4] Explicit stringification. say "$station" gives the same result, as interpolation (the double quotes) stringifies the variable. Note that say $station does not stringify, so we cannot do that here.

Running it:

$ ./minimum-platforms-nicer "11:00 10:00 | 12:01 14:00"
Train departs (11:00) before it arrives (10:00)
  in sub MAIN at ./minimum-platforms-nicer line 62

$ ./minimum-platforms-nicer -l \
  "10:20 10:30 | 11:00 13:20 | 11:10 12:40 | 12:20 12:50 | 16:20 20:20 | 19:00 21:20"
: Visiting train 10:20 -> 10:30
: Visiting train 11:00 -> 13:20
: Visiting train 11:10 -> 12:40
: Visiting train 12:20 -> 12:50
: Visiting train 16:20 -> 20:20
: Visiting train 19:00 -> 21:20
: Station: Grand Central
:  - Platform: 1: 10:20-10:30, 11:00-13:20, 16:20-20:20
:  - Platform: 2: 11:10-12:40, 19:00-21:20
:  - Platform: 3: 12:20-12:50
3

Much nicer.

Note that the «place the train on the first vacant platform» approach does not always work out. Here we have the same arrivals and departures, but specified in a different order:

$ ./minimum-platforms-nicer -l \
  "10:00 10:19 | 10:20 10:39 | 10:40 10:59 | 10:10 10:29 | 10:30 10:49"
: Visiting train 10:00 -> 10:19
: Visiting train 10:20 -> 10:39
: Visiting train 10:40 -> 10:59
: Visiting train 10:10 -> 10:29
: Visiting train 10:30 -> 10:49
: Station: Grand Central
: - Platform: 1: 10:00-10:19, 10:20-10:39, 10:40-10:59
: - Platform: 2: 10:10-10:29, 10:30-10:49
2

$ ./minimum-platforms-nicer -l \
  "10:00 10:19 | 10:30 10:49 | 10:20 10:39 | 10:40 10:59 | 10:10 10:29"
: Visiting train 10:00 -> 10:19
: Visiting train 10:30 -> 10:49
: Visiting train 10:20 -> 10:39
: Visiting train 10:40 -> 10:59
: Visiting train 10:10 -> 10:29
: Station: Grand Central
: - Platform: 1: 10:00-10:19, 10:30-10:49
: - Platform: 2: 10:20-10:39, 10:40-10:59
: - Platform: 3: 10:10-10:29
3

An illustration may help:

The first example to the left (blue), and the second one to the right (green).

The obvious solution, that possibly solves the problem, is to sort the list of trains and insert them in cronological order. That is quite easy:

File: minimum-platforms-sorted (changes only)
my @trains = $trains.split(/\s\|\s+/);  # [2]

for @trains.sort -> $from-to            # [1]
{

[1] Adding sort does the trick.

[2] But we have to remove the leading space(s) in the strings, as the old code (split("|")) left them in place, causing sorting anomalies. (The regex removes the trailing space(s) as well, but that does not really matter.)

Running it on the three platform version above gives the correct two plattform solution:

$ ./minimum-platforms-sorted -l \
  "10:00 10:19 | 10:30 10:49 | 10:20 10:39 | 10:40 10:59 | 10:10 10:29"
: Visiting train 10:00 -> 10:19
: Visiting train 10:20 -> 10:39
: Visiting train 10:40 -> 10:59
: Visiting train 10:10 -> 10:29
: Visiting train 10:30 -> 10:49
: Station: Grand Central
: - Platform: 1: 10:00-10:19, 10:20-10:39, 10:40-10:59
: - Platform: 2: 10:10-10:29, 10:30-10:49
2

But have we really solved the problem? I have not managed to come up with an example that falsifies this assumption, so I am hopeful…

We could try all the permutations of the trains, i.e. the sorting order of the arrival-departure strings, and simply go for the solution with the lowest number of tracks. That is easy(ish):

File: minimum-platforms-permutations (changes only)
unit sub MAIN (Str $trains = "11:20 11:50 | 14:30 15:00",
   :l(:$loquacious),
   :v(:$verbose) = $loquacious,
   :p(:$platforms),
); 
my @trains = $trains.split(/\s\|\s+/);

my $number-of-platforms = Inf;

for @trains.permutations -> @trains-p
{
  my $station = Station.new(name => 'Grand Central');

  for @trains-p -> $from-to
  {
    my (HHMM $from, HHMM $to) = $from-to.words;

    die "Train departs ($from) before it arrives ($to)" if $from gt $to;

    say ": Visiting train $from -> $to" if $verbose;

    $station.add-train($from, $to);
  }

  say $station.Str if $loquacious;

  my $platform-count   = $station.number-of-platforms;
  $number-of-platforms = min($number-of-platforms, $platform-sount);

  say ": Platforms: $platforms" if $platforms;
}

say $number-of-platforms;

Running it:

$ ./minimum-platforms-permutations \
  "10:00 10:19 | 10:30 10:49 | 10:20 10:39 | 10:40 10:59 | 10:10 10:29"
2

With the new «-p» (platforms) option:

$ ./minimum-platforms-permutations \
  "10:00 10:19 | 10:30 10:49 | 10:20 10:39 | 10:40 10:59 | 10:10 10:29"
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 3
: Platforms: 3
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 3
: Platforms: 3
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 2
: Platforms: 3
: Platforms: 2
: Platforms: 2
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
: Platforms: 3
2

A lot of combinations. Actually 120 (5!, or 5 faculty, or [*] 1 .. 5 in Raku). Add the sixth train, and we get 720 combinations. Still doable, perhaps, but the program will certainly not cope with a lot of additional trains.

And that's it.