ETOOBUSY 🚀 minimal blogging for the impatient
PWC128 - Maximum Sub-Matrix
TL;DR
Here we are with TASK #1 from The Weekly Challenge #128. Enjoy!
The challenge
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 ]
The questions
You wish I hadn’t!
Well… not that many actually.
First things first: how do we define maximum? There can be many definitions, e.g. the one with most elements inside, or sorting first by number of rows then by number of columns, or vice-versa… We’ll settle with the number of elements though.
Another one is: should the maximum sub-matrix be exactly the same as pointed out in the examples? I mean, my algorithm for the first input finds this:
[ 0 0 ]
[ 0 0 ]
[ 0 0 ]
which is perfectly valid, as it goes from row 1 column 3 up to row 3
column 4, and has the same number of 0
elements as the example output.
So I’ll assume that yes, any submatrix will do, provided that it has the right size and it is actually contained inside the original matrix, even if it has a different shape than the provided solution. (Maybe the size would have been a better output target, but whatever).
The solution
This is one of those challenges in which I suspect that there’s some clever solution but I optimize for programmer’s time. Well, my time.
I adopted a basic algorithm, based on the observation that any winning
sub-matrix MUST have a 0
in the top-left position. So it makes sense
to find out all positions where there is a `0’ and, for each of them,
compute all possible sub-matrices having that position at the top-left
corner.
This means that I have, roughly speaking, two nested general loops:
one to find all positions with a 0
to use as top-left corner, one to
find all sub-matrices starting from that position and formed by 0
only.
The reality is then a bit more complicated, because we’re dealing with a matrix here, so each general loop is actually formed by two nested loops, one to iterate over rows and the other to iterate over columns. But you get the idea.
Finding all 0
is easy - at least, I think that the code below should
be pretty much self-documenting (it’s sub maximum_sub_matrix
below).
The other one is a bit trickier though, and deserves a few words.
Let’s consider a generic example, isolating only the sub-matrix from the target position up to the end of the matrix (i.e. the lower-right corner):
0 0 0 0 1 .....
0 0 0 0 0 0 1 ...
0 0 0 1 ...
0 0 1 ...
0 1 ....
1 ...
In our algorithm, we proceed row by row. The first row will give us one $(1, x_1)$ candidate sub-matrix, where $x_1$ might potentially arrive up to the end of the row.
In the example, though, $x$ can only arrive up to 4, because after the
fourth position we hit a 1
. So there we have it, the first candidate
is a $(1, 4)$ sub-matrix, whose size is $4$.
Now we add another row. The second row, in particular, has more 0
s
than the first one, but we cannot consider all of them because we have
to extract a $(2, x_2)$ rectangular sub-matrix. We know from the
previous line that we cannot go past $4$, so in this case our iteration
for $x_2$ cannot go beyond that limit. This makes us settle on a $(2,
4)$ candidate, because we have enough 0
to complete a second row like
the first one, for a total size of $8$.
Now we move on adding the third row, i.e. we’re after the best $(3,
x_3)$ candidate starting at the given position. Our $x_3$ has the same
constraint as $x_2$, i.e. it cannot go beyond $4$, but this time we must
stop before because $x_3$ is actually $3$ as we hit a 1
on the fourth
position. This gives us $(3, 3)$ with a size of $9$ and our best
candidate so far.
The following rows go along the same lines: $x_4$ cannot go past $3$
from the previous line, but it stops at $2$ and gives us $(4, 2)$ and a
size of $8$, which is lower than our best of $9$. Last, we add the fifth
line where $x_5$ is equal to 1
and the total size is $5$, which is too
low. We cannot go beyond that row because there’s a 1
. so we stop
there.
All in all, then, in our quest we found out that the best candidate has size $9$ and dimensions $(3, 3)$.
In this last days I studied a lot of Raku, so it just seemed right to start with a Perl implementation:
#!/usr/bin/env perl
use v5.24;
use warnings;
use experimental 'signatures';
no warnings 'experimental::signatures';
sub maximum_submatrix_at ($M, $y, $x) {
my $target = $M->[$y][$x];
my ($max_size, @best) = (0) x 3;
my $max_x = $M->[$y]->$#*;
for my $Y ($y .. $M->$#*) {
last if $M->[$Y][$x] ne $target;
my $y_size = $Y - $y + 1;
my $size = 0;
for my $X ($x .. $max_x) {
if ($M->[$Y][$X] ne $target) {
$max_x = $X - 1;
last;
}
$size += $y_size;
if ($size > $max_size) {
$max_size = $size;
@best = ($y_size, $X - $x + 1);
}
}
}
return ($max_size, @best);
}
sub maximum_submatrix ($M, $target = '0') {
my ($max, @best) = (0);
for my $y (0 .. $M->$#*) {
for my $x (0 .. $M->[$y]->$#*) {
next unless $M->[$y][$x] eq $target;
my ($size, @round) = maximum_submatrix_at($M, $y, $x);
($max, @best) = ($size, @round) if $size > $max;
}
}
return [map {[(0) x $best[1]]} 1 .. $best[0]];
}
sub print_matrix ($M) {
for my $row ($M->@*) {
say '[ ', join(' ', $row->@*), ' ]';
}
}
my @Ms = (
[
[ 1, 0, 0, 0, 1, 0, ],
[ 1, 1, 0, 0, 0, 1, ],
[ 1, 0, 0, 0, 0, 0, ],
],
[
[ 0, 0, 1, 1, ],
[ 0, 0, 0, 1, ],
[ 0, 0, 1, 0, ],
],
[
[ 0, 1, 0, 1, ],
[ 1, 0, 1, 0, ],
[ 0, 1, 0, 1, ],
[ 1, 0, 1, 0, ],
],
[
[ 1, 0, 0, 0, 1, 0, ],
[ 1, 0, 1, 0, 0, 1, ],
[ 1, 0, 0, 0, 0, 0, ],
],
);
for my $M (@Ms) {
say '';
print_matrix($M);
say '---';
print_matrix(maximum_submatrix($M));
say "\n--------\n";
}
Sub maximum_sub_matrix
deals with the outer loop(s), while
maximum_sub_matrix_at
deals with the inner loop(s). No big deal.
It does not take too much to translate this in Raku:
#!/usr/bin/env raku
use v6;
sub maximum-submatrix-at (@M, $y, $x) {
my $target = @M[$y][$x];
my ($max-size, @best) = 0 xx 3;
my $max-x = @M[$y].end;
for $y .. @M.end -> $Y {
last if @M[$Y][$x] ne $target;
my $y-size = $Y - $y + 1;
my $size = 0;
for $x .. $max-x -> $X {
if @M[$Y][$X] ne $target {
$max-x = $X - 1;
last;
}
$size += $y-size;
if ($size > $max-size) {
$max-size = $size;
@best = ($y-size, $X - $x + 1);
}
}
}
return ($max-size, |@best);
}
sub maximum-submatrix (@M, $target = '0') {
my ($max, @best) = (0);
for 0 .. @M.end -> $y {
for 0 .. @M[$y].end -> $x {
next unless @M[$y][$x] eq $target;
my ($size, @round) = maximum-submatrix-at(@M, $y, $x);
($max, @best) = ($size, |@round) if $size > $max;
}
}
return [(1 .. @best[0]).map: { [ 0 xx @best[1] ] }];
}
sub print-matrix (@M) {
for @M -> @row {
put '[ ', @row.join(' '), ' ]';
}
}
my @Ms = (
[
[ 1, 0, 0, 0, 1, 0, ],
[ 1, 1, 0, 0, 0, 1, ],
[ 1, 0, 0, 0, 0, 0, ],
],
[
[ 0, 0, 1, 1, ],
[ 0, 0, 0, 1, ],
[ 0, 0, 1, 0, ],
],
[
[ 0, 1, 0, 1, ],
[ 1, 0, 1, 0, ],
[ 0, 1, 0, 1, ],
[ 1, 0, 1, 0, ],
],
[
[ 1, 0, 0, 0, 1, 0, ],
[ 1, 0, 1, 0, 0, 1, ],
[ 1, 0, 0, 0, 0, 0, ],
],
);
for @Ms -> @M {
put '';
print-matrix(@M);
put '---';
print-matrix(maximum-submatrix(@M));
put "\n--------\n";
}
And with this… that’s all, folks!