About a year ago when I was applying to jobs for the first time, I had an interview at a company and they posed the following problem to me, which I preceded to bomb.
A year later I actually came up with a solution to the problem, and I couldn't be happier. However, I would love for some critiques as to the design of my solution or other feedback about: style, OOP practices, etc..
Problem Statement
A group of farmers has some elevation data, and we're going to help them understand how rainfall flows over their farmland.
We'll represent the land as a two-dimensional array of altitudes and use the following model, based on the idea that water flows downhill:
If a cell’s four neighboring cells all have higher altitudes, we call this cell a sink; water collects in sinks.
Otherwise, water will flow to the neighboring cell with the lowest altitude. If a cell is not a sink, you may assume it has a unique lowest neighbor and that this neighbor will be lower than the cell.
Cells that drain into the same sink – directly or indirectly – are said to be part of the same basin.
Your challenge is to partition the map into basins. In particular, given a map of elevations, your code should partition the map into basins and output the sizes of the basins, in descending order.
Assume the elevation maps are square. Input will begin with a line with one integer, S, the height (and width) of the map. The next S lines will each contain a row of the map, each with S integers – the elevations of the S cells in the row. Some farmers have small land plots such as the examples below, while some have larger plots. However, in no case will a farmer have a plot of land larger than S = 5000.
Your code should output a space-separated list of the basin sizes, in descending order. (Trailing spaces are ignored.)
A few examples are below:
----------------------------------------- Input: Output: 3 7 2 1 5 2 2 4 7 3 6 9 The basins, labeled with A’s and B’s, are: A A B A A B A A A ----------------------------------------- Input: Output: 1 1 10 There is only one basin in this case. The basin, labeled with A’s is: A ----------------------------------------- Input: Output: 5 11 7 7 1 0 2 5 8 2 3 4 7 9 3 5 7 8 9 1 2 5 4 3 3 3 5 2 1 The basins, labeled with A’s, B’s, and C’s, are: A A A A A A A A A A B B A C C B B B C C B B C C C ----------------------------------------- Input: Output: 4 7 5 4 0 2 1 3 2 1 0 4 3 3 3 3 5 5 2 1 The basins, labeled with A’s, B’s, and C’s, are: A A B B A B B B A B B C A C C C -----------------------------------------
My solution, written in Perl, is as follows:
Cell
class (models individual cells in the matrix):
{
package Cell;
use List::MoreUtils qw(all);
# models 4 nearest neighbors to position i,j forall i,j
my $neighbors = [ [ 0, -1], # left
[-1, 0], # top
[ 0, 1], # right
[+1, 0], # bottom
];
sub new {
my ($class, %attrs) = @_;
$attrs{is_sink} ||= 0;
bless \%attrs, $class;
}
# accessor/setter methods
sub elevation {
return shift->{elevation};
}
sub x {
return shift->{x};
}
sub y {
return shift->{y};
}
sub is_sink {
return shift->{is_sink};
}
sub set_sink {
shift->{is_sink} = 1;
}
sub xy {
my $self = shift;
return [$self->x, $self->y];
}
# string representation of a Cell
sub to_s {
my ($self) = @_;
return "(" . $self->x . "," . $self->y . ") = " . $self->elevation;
}
# returns the neighbors that flow into this Cell
sub get_flowing_neighbors {
my ($self, $rainfall) = @_;
# the neighbors of this cell flow into it
# iff this cell's elevation is less than their neighbors
# AND the neighboring cell has no other neighbors
# (that are not this cell) that have a lower (or equal) elevation
return grep {
$self->elevation < $_->elevation
&& all { $self->elevation <= $_->elevation } $_->get_neighbors($rainfall)
} $self->get_neighbors($rainfall);
}
# returns the neighbors of this Cell
sub get_neighbors {
my ($self, $rainfall) = @_;
my ($rows, $cols) = ($rainfall->rows, $rainfall->cols);
my ($x, $y) = ($self->x, $self->y);
my @adjs;
NEIGHBORS:
for my $neighbor ( @$neighbors ) {
my ($xmod, $ymod) = ($x + $neighbor->[0], $y + $neighbor->[1]);
# x and y must be in the bounds of the matrix
next NEIGHBORS
if $xmod > $rows - 1 || $ymod > $cols - 1 || $xmod < 0 || $ymod < 0;
push @adjs,
$rainfall->cell($xmod, $ymod);
}
return @adjs;
}
1;
} # end Cell
Rainfall
class (models the entire matrix and operations across it):
{
package Rainfall;
sub new {
my ($class, %attrs) = @_;
# initialize all elements of the matrix to cells O(n)
for my $i ( 0 .. @{ $attrs{field} } - 1) {
for my $j ( 0 .. @{ $attrs{field}->[$i] } - 1 ) {
$attrs{field}->[$i]->[$j] =
Cell->new( x => $i,
y => $j,
elevation => $attrs{field}->[$i]->[$j],
);
}
}
bless \%attrs, $class;
}
# accessor methods
sub field {
my $self = shift;
return $self->{field};
}
sub cell {
my ($self, $i, $j) = @_;
return $self->field->[$i]->[$j];
}
sub rows {
my $self = shift;
return $self->{rows};
}
sub cols {
my $self = shift;
return $self->{cols};
}
# determines if a given Cell is a sink
sub is_sink {
my ($self, $cell) = @_;
my $min = $cell->elevation;
my @neighbors = $cell->get_neighbors($self);
for my $neighbor ( @neighbors ) {
$min = ($min, $neighbor->elevation)[$min > $neighbor->elevation];
}
# found a sink, mark it
if( $min == $cell->elevation ) {
$cell->set_sink;
return 1;
}
return 0;
}
# returns a list of all Sinks in the matrix
# O(N * M) where N = # of rows and M = # of cols
sub find_sinks {
my ($self) = @_;
my @sinks;
for my $row ( 0 .. $self->rows - 1 ) {
for my $cell ( @{ $self->field->[$row] } ) {
push @sinks, $cell
if $self->is_sink($cell);
}
}
return @sinks;
}
# given an Array of Sinks, find the Basins in this field
# O(n)
sub find_basins_from_sinks {
my ($self, @sinks) = @_;
my %basin;
my $basin_marker = 'A';
# determine how many cells eventually flow into this one
for my $sink ( @sinks ) {
$basin{$basin_marker++} = $self->basin_size($sink);
}
return %basin;
}
# recursively find the number of Cells in the Basin
# attached to the given Cell
sub basin_size {
my ($self, $cell) = @_;
my $size = 1;
for my $neighbor ( $cell->get_flowing_neighbors($self) ) {
$size += $self->basin_size($neighbor);
}
return $size;
}
1;
} # end Rainfall
Unit tests that verify results to match the example cases above:
{ # Tests
use Test::More tests => 4;
{ # 3x3 field
my $r = Rainfall->new( rows => 3,
cols => 3,
field => [ [1, 5, 2],
[2, 4, 7],
[3, 6, 9] ]);
my @sinks = $r->find_sinks;
my %basin = $r->find_basins_from_sinks(@sinks);
is_deeply(
[sort { $b <=> $a } values %basin],
[7, 2],
'Correctly divided 3x3 field into 2 basins'
);
}
{ # 1x1 field
my $r = Rainfall->new( rows => 1,
cols => 1,
field => [ [1] ]);
my @sinks = $r->find_sinks;
my %basin = $r->find_basins_from_sinks(@sinks);
is_deeply(
[sort { $b <=> $a } values %basin],
[1],
'Correctly divided 1v1 field into 1 basin'
);
}
{ # 5x5 field
my $r = Rainfall->new( rows => 5,
cols => 5,
field => [ [1, 0, 2, 5, 8],
[2, 3, 4, 7, 9],
[3, 5, 7, 8, 9],
[1, 2, 5, 4, 3],
[3, 3, 5, 2, 1] ]);
my @sinks = $r->find_sinks;
my %basin = $r->find_basins_from_sinks(@sinks);
is_deeply(
[sort { $b <=> $a } values %basin],
[11, 7, 7],
'Correctly divided 5v5 field into 3 basins'
);
}
{ # Test 4x4 field
my $r = Rainfall->new( rows => 4,
cols => 4,
field => [ [0, 2, 1, 3],
[2, 1, 0, 4],
[3, 3, 3, 3],
[5, 5, 2, 1] ]);
my @sinks = $r->find_sinks;
my %basin = $r->find_basins_from_sinks(@sinks);
is_deeply(
[sort { $b <=> $a } values %basin],
[7, 5, 4],
'Correctly divided 4v4 field into 3 basins'
);
}
}
The basic algorithm searches through each element in the matrix and determines if that element is a sink (meaning that all of its neighbors flow into it).
sinks = []
for i in matrix: # rows
for j in matrix[i]: # columns
if matrix[i][j] is a sink:
sinks.add( matrix[i][j] )
From the problem description you can tell that there will be one Basin
per sink, so after you have the sinks you need to find the Basin
s. To find Basin
s you start at the Sink
s and search outward, adding elements to the basin if they A) flow into the sink or B) flow into a cell that flows into a sink. You stop searching outward when there are no more flow paths to consider.
def find_basins_from_sinks(sinks):
basins = {} # map
marker = 'A'
for sink in sinks:
basins[marker++] = basin_size(sink)
def basin_size(cell):
size = 1
for neighbor in cell.neighbors:
size += basin_size(neighbor)