This is my response to the Perl Weekly Challenge #128.
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 ]
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.