2008 Practice Problems

I play-tested the
Practice Problems with
C, while a friend used Python. I was handily beaten: I recall being slowed down
by trivial printf and scanf bugs, and later running into a larger roadblock
when I realized I needed multiprecision arithmetic.

Until then, I thought I was fast at typing administrative details like
declarations, semicolons, ampersands, memory allocations, and so on. I was
confident the cost was negligible. But the devil was in the details: in the end
I was trounced precisely because they got in my way.

My dismal performance shook me out of an illogical mindset that I perhaps
inherited from the 1980s. Although the contest was happy with, say, a program
that took one minute to solve a problem, for some reason, I wanted my programs
to be so fast that small cases finish instantly from a human’s perspective.
Even on the latest hardware, popular scripting languages can take half a second
just to start up, so I was stuck with C.

With Haskell, I can have my cake and eat it too: lack of bookkeeping and
boilerplate means I can focus on algorithms, yet I still have type safety and
fast compiled code. If only I knew then what I know now.

The problem description is a cute way of saying we want to convert from one
base to another. We could maybe use unfoldr with divMod to convert to the
second base, which would prettily complement the fold used to convert from the
first base, but this might be more verbose.

The sole job of one helper function is to handle 0 correctly, where we wish to
print the zero digit instead of the empty string.

I forget if I ever got around to programming this in C or not, but I’m sure
glad I’m using Haskell this time around!

The algorithm is straightforward. We imagine we have a pen and paper with
grid lines. We start at some point on the grid which we label (0, 0), and
trace out the path from entrance to exit. Along the way, we mark the walls
that our left hand touches. For example, if we walk straight ahead, then
there must be a wall to our left, while if we turned right twice before walking
straight ahead, then there must be three walls, one in every direction except
for the way we entered the square.

After tracing the path, we know the location of the exit of the maze, and
its direction, so we can simply reverse direction and follow the path from
the exit to the entrance, and again mark the walls touched by our left hand
as we go.

The conditions of the problem imply that we have touched every wall in the maze
at least once by now, so we finish by printing the walls we found.

We use a Data.Map to store the squares of the grid as we walk across them.
Later, we find the minimum and maximum of the rows and columns, and iterate
on all values in their ranges to print the walls. Some care is needed because
a square may have no walls in which case it is absent from the map: we handle
this by calling fromMaybe 0 on the results of lookup.

Each map entry holds an Int whose bits represent the walls that are present.
The directions are ordered according to the table given in the problem.

The endBy function of Data.List.Split coupled with pattern matching yields
succinct, clear code to handle the different kinds of turns and the following
step.

There’s little else to describe. The h function takes the direction we
are currently facing and marks the wall touched by the left hand. The g
function turns to the given direction, takes one step, and also accumlates the
given walls into a list. We use fromListWith along with a bitwise OR to
convert this list into the map described above.

This problem builds on a famous question reputedly encountered by programmers
interviewing for a job.

The solution hinges on a simple recurrence. Let fmax d b be the maximum
number of floors we can distinguish with at most d drops and allowing up
to b breaks.

Suppose we drop our first egg from floor f. If it breaks, we know the
highest floor from which we may safely drop an egg is less than f and
furthermore we have d - 1 remaining drops and b - 1 remaining breaks
to find it.

On the other hand, if the egg remains intact then we know the critical floor
is strictly above f, and we have d - 1 remaining drops and b remaining
breaks to find it. Thus we have:

fmax d b = fmax (d - 1) (b - 1) + 1 + fmax (d - 1) b

As for the base cases: if we have no remaining drops or breaks then we are
forbidden to drop any eggs, so we learn nothing:

For the large input, we handle a few cases specially. Firstly, when we may
break at most one egg, the only possible course of action is to drop the
egg on every floor until it breaks, starting from floor 1 and moving up:

fmax d 1 = d

Secondly, if we have two egg breaks available, then if we have more than
sqrt(2 * 2^32) drops available, then we can handle over 2^32 floors.
One can prove this, or write a program to find that 92682 is the limit:

head [d | d <- [0..], fmax d 2 >= 2^32]

Since more available breaks means even higher floor limits, we have
fmax d b = -1 for d > 92681 and b > 1.

Lastly, we check as early as possible for floor limits that are at least
2^32.

For now, assume there are no perishable items.
We must find the optimal order and locations to buy them.
This is somewhat like the
Travelling Salesman
Problem in that we can use dynamic programming to improve on the naive
algorithm by recursing on subsets instead of permutations of subsets.

Define f items pos to be the mininum cost of buying each member of items
starting from the position pos then returning home. Then:

That is, for each member i of items and for each store j that sells i,
we consider buying i at store j first then buying the rest. Then the
optimal way to buy items is the cheapest of these options. Here, the
dist function multiplies the distance between two given positions by
the cost of gas.

Perishable items add a wrinkle to the algorithm. We must remember whether we
just bought a perishable item along with the items we have already bought and
our starting position, that is, we now consider a cost function with three
parameters:

We use a bitset to represent items, with 0 represent those we wish to
acquire. The iMap maps items to lists of (store, price) tuples. The position
is an index into a vector holding each store’s location, except for -1 which
represents the origin.

As usual, Data.MemoTrie takes care of top-down memoization. The program
is barely fast enough, taking 7 minutes to run on my laptop. A bottom-up
array may be faster to build.