Maximal Right
with Raku

by Arne Sommer

Maximal Right with Raku

[319] Published 7. December 2024.

This is my response to The Weekly Challenge #298.

Challenge #298.1: Maximal Square

You are given an m x n binary matrix with 0 and 1 only.

Write a script to find the largest square containing only 1's and return it’s area.

Example 1:
Input: @matrix = ([1, 0, 1, 0, 0],
                  [1, 0, 1, 1, 1],
                  [1, 1, 1, 1, 1],
                  [1, 0, 0, 1, 0])
Output: 4

Two maximal square found with same size marked as 'x':

[1, 0, 1, 0, 0]
[1, 0, x, x, 1]
[1, 1, x, x, 1]
[1, 0, 0, 1, 0]

[1, 0, 1, 0, 0]
[1, 0, 1, x, x]
[1, 1, 1, x, x]
[1, 0, 0, 1, 0]
Example 2:
Input: @matrix = ([0, 1],
                  [1, 0])
Output: 1

Two maximal square found with same size marked as 'x':

[0, x]
[1, 0]


[0, 1]
[x, 0]
Example 3:
Input: @matrix = ([0])
Output: 0

This is a mutation of Challenge #128.1: Maximum Sub-Matrix, except that we looked for zeroes then, and the result must be a square this time.

Brute force, together with some cleverness, should do the trick.

File: maximal-square
#! /usr/bin/env raku

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

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

die "Illegal characters" unless all(@matrix[*;*]) ~~ 0 | 1;       # [3]

die "Uneven row length" unless [==] @matrix>>.elems;              # [4]

my $rows = @matrix.elems;                                         # [5]
my $cols = @matrix[0].elems;                                      # [6]
my $max  = 0;                                                     # [7]

for ^$rows -> $row                                                # [8]
{
  for ^$cols -> $col                                              # [9]
  {
    if @matrix[$row][$col] == 0                                   # [10]
    {
      say ": [r:$row,c:$col] - Skipping zero" if $verbose;
      next;                                                       # [10a]
    }

    my $max-size = min($rows - $row, $cols - $col);               # [11]

    say ": [r:$row,c:$col] maxbox: $max-size" if $verbose;

OUTERBOX:
    for 1 .. $max-size -> $size                                   # [12]
    {
      for $row .. $row + $size -1 -> $r                           # [13]
      {
        for $col .. $col + $size -1 -> $c                         # [14]
        {
	  say ": [r:$r,c:$c] -> @matrix[$r][$c]" if $verbose;
          last OUTERBOX if @matrix[$r][$c] == 0;                  # [15]
        }
      }

      say ": Found 1-box $size x $size at [$row,$col]" if $verbose;

      $max = max($max, $size * $size)                             # [16]
    }
  }
}

say $max;                                                         # [17]

[1] Note the «matriux as a string» syntax, used here with the first example as the default value.

[2] Unpack the string into a two dimentional array. The Numeric part is there to get rid of the undecided IntStr type, which looks pretty bad in verbose output. (Using int would have coerced non-integer numeric values to integers, and we do not want that type of errors to slip through the cracks.)

[3] Ensure that we get the binary digits 0 and 1 only. The all Junction needs a one dimentional array, which we get with [*;*].

[4] All the matrix rows must have the same length.

[5] Get the number of rows.

[6] Get the number of columns.

[7] The current max value, initially zero.

[8] We are doing this the hard way, by iterating over every position in the matrix (to be used as the upper left hand corner of a square sub matrix). We start with the rows,

[9] then the columns.

[10] Skip the current position [10a] if the cell has a value of zero (as we require a box of 1s).

[11] This is the maximum box size (as length of the sides), given the size of the matrix.

[12] Iterate over all possible box sizes, for the given upper left hand corner of a square. We start with a size of 1, so that we can short circuit this outer loop if we encounter a zero.

[13] Iterate over the rows,

[14] and the columns.

[15] Short circuit the entire loop if we encounter a zero.

[16] We will only get here if we failure to fail (in [15]). That means success. Save the new max value if it is larger than the old one.

[17] Print the result.

Running it:

$ ./maximal-square
4

$ ./maximal-square "0 1 | 1 0"
1

$ ./maximal-square "0"
0

Looking good.

With verbose mode:

$ ./maximal-square -v 
: [r:0,c:0] maxbox: 4
: [r:0,c:0] -> 1
: Found 1-box 1 x 1 at [0,0]
: [r:0,c:0] -> 1
: [r:0,c:1] -> 0
: [r:0,c:1] - Skipping zero
: [r:0,c:2] maxbox: 3
: [r:0,c:2] -> 1
: Found 1-box 1 x 1 at [0,2]
: [r:0,c:2] -> 1
: [r:0,c:3] -> 0
: [r:0,c:3] - Skipping zero
: [r:0,c:4] - Skipping zero
: [r:1,c:0] maxbox: 3
: [r:1,c:0] -> 1
: Found 1-box 1 x 1 at [1,0]
: [r:1,c:0] -> 1
: [r:1,c:1] -> 0
: [r:1,c:1] - Skipping zero
: [r:1,c:2] maxbox: 3
: [r:1,c:2] -> 1
: Found 1-box 1 x 1 at [1,2]
: [r:1,c:2] -> 1
: [r:1,c:3] -> 1
: [r:2,c:2] -> 1
: [r:2,c:3] -> 1
: Found 1-box 2 x 2 at [1,2]
: [r:1,c:2] -> 1
: [r:1,c:3] -> 1
: [r:1,c:4] -> 1
: [r:2,c:2] -> 1
: [r:2,c:3] -> 1
: [r:2,c:4] -> 1
: [r:3,c:2] -> 0
: [r:1,c:3] maxbox: 2
: [r:1,c:3] -> 1
: Found 1-box 1 x 1 at [1,3]
: [r:1,c:3] -> 1
: [r:1,c:4] -> 1
: [r:2,c:3] -> 1
: [r:2,c:4] -> 1
: Found 1-box 2 x 2 at [1,3]
: [r:1,c:4] maxbox: 1
: [r:1,c:4] -> 1
: Found 1-box 1 x 1 at [1,4]
: [r:2,c:0] maxbox: 2
: [r:2,c:0] -> 1
: Found 1-box 1 x 1 at [2,0]
: [r:2,c:0] -> 1
: [r:2,c:1] -> 1
: [r:3,c:0] -> 1
: [r:3,c:1] -> 0
: [r:2,c:1] maxbox: 2
: [r:2,c:1] -> 1
: Found 1-box 1 x 1 at [2,1]
: [r:2,c:1] -> 1
: [r:2,c:2] -> 1
: [r:3,c:1] -> 0
: [r:2,c:2] maxbox: 2
: [r:2,c:2] -> 1
: Found 1-box 1 x 1 at [2,2]
: [r:2,c:2] -> 1
: [r:2,c:3] -> 1
: [r:3,c:2] -> 0
: [r:2,c:3] maxbox: 2
: [r:2,c:3] -> 1
: Found 1-box 1 x 1 at [2,3]
: [r:2,c:3] -> 1
: [r:2,c:4] -> 1
: [r:3,c:3] -> 1
: [r:3,c:4] -> 0
: [r:2,c:4] maxbox: 1
: [r:2,c:4] -> 1
: Found 1-box 1 x 1 at [2,4]
: [r:3,c:0] maxbox: 1
: [r:3,c:0] -> 1
: Found 1-box 1 x 1 at [3,0]
: [r:3,c:1] - Skipping zero
: [r:3,c:2] - Skipping zero
: [r:3,c:3] maxbox: 1
: [r:3,c:3] -> 1
: Found 1-box 1 x 1 at [3,3]
: [r:3,c:4] - Skipping zero
4

$ ./maximal-square -v "0 1 | 1 0"
: [r:0,c:0] - Skipping zero
: [r:0,c:1] maxbox: 1
: [r:0,c:1] -> 1
: Found 1-box 1 x 1 at [0,1]
: [r:1,c:0] maxbox: 1
: [r:1,c:0] -> 1
: Found 1-box 1 x 1 at [1,0]
: [r:1,c:1] - Skipping zero
1

$ ./maximal-square -v "0"
: [r:0,c:0] - Skipping zero
0

Challenge #298.2: Right Interval

You are given an array of @intervals, where $intervals[i] = [starti, endi] and each starti is unique.

The right interval for an interval i is an interval j such that startj >= endi and startj is minimized. Please note that i may equal j.

Write a script to return an array of right interval indices for each interval i. If no right interval exists for interval i, then put -1 at index i.

Example 1:
Input: @intervals = ([3,4], [2,3], [1,2])
Output: (-1, 0, 1)

There is no right interval for [3,4].
The right interval for [2,3] is [3,4] since start0 = 3 is the smallest
  start that is >= end1 = 3.
The right interval for [1,2] is [2,3] since start1 = 2 is the smallest
  start that is >= end2 = 2.
Example 2:
Input: @intervals = ([1,4], [2,3], [3,4])
Output: (-1, 2, -1)

There is no right interval for [1,4] and [3,4].
The right interval for [2,3] is [3,4] since start2 = 3 is the smallest
  start that is >= end1 = 3.
Example 3:
Input: @intervals = ([1,2])
Output: (-1)

There is only one interval in the collection, so it outputs -1.
Example 4:
Input: @intervals = ([1,4], [2, 2], [3, 4])
Output: (-1, 1, -1)

File: right-interval
#! /usr/bin/env raku

unit sub MAIN (Str $intervals = "3 4 | 2 3 | 1 2",                # [1]
               :v(:$verbose));

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

die "Illegal characters"
  unless all(@intervals[*;*]) ~~ Int;                             # [3]

die "Interval must have two values only"
  unless all(@interval>>.elems) == 2;                             # [4]

say ": Intervals: { @intervals.raku }" if $verbose;

my %start2index;                                                  # [5]
my $index = 0;                                                    # [6]

for @intervals -> @interval                                       # [7]
{
  die "Interval ends before the start"
    if @interval[0] > @interval[1];                               # [8]
  
  die "Interval start @interval[0] already used"                  # [9]
    if defined %start2index{@interval[0]};

  %start2index{@interval[0]} = $index;                            # [10]

  $index++;                                                       # [11]
}

say ": Start2index: { %start2index.raku }" if $verbose;

my @starts = %start2index.keys.sort;                              # [12]

say ": Start values: { @starts.raku }" if $verbose;

my @output;                                                       # [13]

for @intervals -> @interval                                       # [14]
{
  my $end  = @interval[1];                                        # [15]
  my @next = @starts.grep: * >= $end;                             # [16]

  @output.push( @next.elems ?? %start2index{@next.first} !! -1);  # [17]
}

say "({ @output.join(", ") })";                                   # [18]

[1] Note the default value; this is the first example.

[2] Turn the input into a two dimentional array.

[3] Ensure that we get Integers only.

[4] Every sub array must have two elements.

[5] The index of each start value (of the pairs) will end up here.

[6] The current index.

[7] Iterate over the intervals.

[8] The start value cannot be higher than the end value.

[9] The start value must be unique.

[10] Note the index of the current start value.

[11] Adjust the index in preparation for the next iteration.

[12] Get the start values of the intervals, in sorted order. (We could also have used something like: @intervals.map({ $_[0]}).sort }) .)

[13] The result (indices) will end up here.

[14] Iterate over the intervals, for a second time.

[15] Get the end of the current interval.

[16] Get the start values that are equal to or larger than the current end value.

[17] Do we have any elements? If so, take the first one (with first) and save the corresponding index. if not, save -1.

See docs.raku.org/routine/first for more information about first.

[18] Pretty print the result.

Running it:

$ ./right-interval
(-1, 0, 1)

$ ./right-interval "1 4 | 2 3 | 3 4"
(-1, 2, -1)

$ ./right-interval "1 2"
(-1)

$ ./right-interval "1 4 | 2 2 | 3 4"
(-1, 1, -1)

Looking good.

With verbose mode:

$ ./right-interval -v
: Intervals: [(3, 4), (2, 3), (1, 2)]
: Start2index: {"1" => 2, "2" => 1, "3" => 0}
: Start values: ["1", "2", "3"]
(-1, 0, 1)

$ ./right-interval -v "1 4 | 2 3 | 3 4"
: Intervals: [(1, 4), (2, 3), (3, 4)]
: Start2index: {"1" => 0, "2" => 1, "3" => 2}
: Start values: ["1", "2", "3"]
(-1, 2, -1)

$ ./right-interval -v "1 2"
: Intervals: [(1, 2),]
: Start2index: {"1" => 0}
: Start values: ["1"]
(-1)

$ ./right-interval -v "1 4 | 2 2 | 3 4"
: Intervals: [(1, 4), (2, 2), (3, 4)]
: Start2index: {"1" => 0, "2" => 1, "3" => 2}
: Start values: ["1", "2", "3"]
(-1, 1, -1)

We can avoid -1 in the result, but some or all of the intervals will point back to themselves:

$ ./right-interval -v "1 1 | 2 2 | 3 3"
: Intervals: [(1, 1), (2, 2), (3, 3)]
: Start2index: {"1" => 0, "2" => 1, "3" => 2}
: Start values: ["1", "2", "3"]
(0, 1, 2)

$ ./right-interval -v "1 3 | 2 3 | 3 3"
: Intervals: [(1, 3), (2, 3), (3, 3)]
: Start2index: {"1" => 0, "2" => 1, "3" => 2}
: Start values: ["1", "2", "3"]
(2, 2, 2)

And that's it.