I’ve been doing the Perl Weekly
Challenges. The
latest
involved packing an array and a standard geometry problem. (Note that
this is open until 28 February 2021.)
TASK #1 › Pack a Spiral
You are given an array @A
of items (integers say, but they can be
anything).
Your task is to pack that array into an MxN matrix spirally
counterclockwise, as tightly as possible.
‘Tightly’ means the absolute value |M-N| of the difference has to
be as small as possible.
I see two parts to this problem: first determine the appropriate
dimensions for the array, then do the packing.
The factors of the number of items that are closest to equal will
correspond to the shape closest to a square. So I solve part 1 by
taking the highest integer less than or equal to the square root of
the number of items, checking for divisibility into that number, then
decrementing until that test succeeds. (In the limiting case it'll
always work when the integer reaches 1. If this were a real-world
problem I might do a prime factorisation instead and binary walk the
result.)
my $n=scalar @a;
my $f2=int(sqrt($n));
my $f1;
while (1) {
if ($n % $f2 == 0) {
$f1=$n/$f2;
last;
}
$f2--;
}
Then fill the output array with zeroes, or whatever, just to get it to
the right size. (In the languages that have types, there are ways of
being generic about the array contents, but I didn't bother.)
my @out;
foreach (1..$f2) {
push @out,[(0) x $f1];
}
Then it's time to walk the array filling it up. Clearly there are four
filling directions, and enough differs between them (which value I'm
changing, in which direction, what's the limiting value and condition)
that I ended up just writing the four separately, though it might be
an amusing exercise in lambdas. (Each method breaks out of the overall
loop if it runs out of list entries.)
my ($x,$y)=(-1,0);
my $maxx=$f1-1;
my $maxy=$f2-1;
my $minx=0;
my $miny=1;
ARR:
while (1) {
while ($x < $maxx) {
$x++;
$out[$y][$x]=pop @a;
unless (@a) {
last ARR;
}
}
$maxx-=1;
while ($y < $maxy) {
$y++;
$out[$y][$x]=pop @a;
unless (@a) {
last ARR;
}
}
$maxy-=1;
while ($x > $minx) {
$x--;
$out[$y][$x]=pop @a;
unless (@a) {
last ARR;
}
}
$minx++;
while ($y > $miny) {
$y--;
$out[$y][$x]=pop @a;
unless (@a) {
last ARR;
}
}
$miny++;
}
Other languages are basically similar. Python can't break out of an
inner loop (by design apparently), and Ruby does it with
throw
/catch
. Rust needs my subscripts to be of a usize
type,
which can't be set to -1 ever, so that needs a flag for the first
pass incrementing x.
TASK #2 › Origin-containing Triangle
You are given three points in the plane, as a list of six
co-ordinates: A=(x1,y1), B=(x2,y2) and C=(x3,y3).
Write a script to find out if the triangle formed by the given three
co-ordinates contain origin (0,0).
Print 1 if found otherwise 0.
I felt free to declare that what I was getting was a list of three
coordinate pairs, not a single flat list of six numbers, and code on
that basis.
This is a standard geometry problem (well, it's usually to an
arbitrary point rather than the origin), with two solutions I can
think of: one is to build a new triangle from each line to the origin,
measure the areas of each, and compare with the area of the original
triangle, but that would potentially mean doing floating-point
comparisons. So instead I used the other approach: take the cross
products of the vectors (point A to origin) and (point A to point B)
(actually just the z component of that cross product, since they're
coplanar in x/y) which is effectively a function of the angle between
them.
(What do you get if you cross a dog with a cat? |dog| |cat| sin
theta.)
If all those cross products are positive (or zero in the case where
the origin lies along one of the edges), or if they're all
negative-or-zero depending on how the calculation was done, then the
origin lies inside the triangle. If some cross products are positive
and others are negative, it's outside.
Part two is usually meant to be the harder part of the pair of
problems, but the hard bit has already been achieved in working out
the above; the actual coding is relatively simple. In Raku:
sub ot(@points) {
my @pp=@points;
push @pp,@points[0];
my @xp;
for (0..2) -> $i {
push @xp,(@pp[$i][0] *
(@pp[$i+1][1]-@pp[$i][1]))
-
(@pp[$i][1] *
(@pp[$i+1][0]-@pp[$i][0]));
}
@xp=sort @xp;
if (@xp[0] <=0 && @xp[2] <=0) {
return 1;
}
if (@xp[0] >=0 && @xp[2] >=0) {
return 1;
}
return 0;
}
(This is a good example, I think, of when not to make a thing a
function – the cross product calculation is an obvious candidate, but
in this specific code I'm only using it in this one place so there's
no particular virtue to splitting it out. In real life I'd probably be
coding this up as part of a geometry library, in which case there
would most certainly be a separate cross product function that would
be called from many places.)
Full code on
github.
Comments on this post are now closed. If you have particular grounds for adding a late comment, comment on a more recent post quoting the URL of this one.