Visualising a water filling algorithm
2013-11-01
I was looking at the water filling problem described at “I Failed a Twitter Interview”. The problem is, given a topography expressed as an array of heights, to figure out the volume of water that will pool if it rains on that topography. I’m generally fairly rubbish at puzzles, and my solution to the problem only worked for a particular subset of the problem.
I was then trying to understand the solution the author presented, so I wrote a visualisation of it. I’ve included the code below, it produces a move-by-move visualisation of the state of the solver. Writing it helped me understand the elegance of the presented by the solution.
The output looks like the following (where ~ represents water):
_
_ _ |8|
|7|~~~|7|~_~~~~~~~| | _
| |~~~| ||6|~~~~_~| |~~~~_~|6|
| |~~~| || |~_~|5|| |~~~|5|| |
| |~_~| || ||4|| || |~~~| || | _ _
_ | ||3|| || || || || |~~~| || |~~~|3||3| _
|2|| || || || || || || |~~~| || |~~~| || ||2|
| || || || || || || || |~_~| || |~_~| || || |
Current Volume: 20
Code:
import java.util.Random;
public class WaterLevel
{
private static final long PER_MOVE_SLEEP = 300;
public static void main(String[] args) throws java.lang.Exception
{
calculateVolume(generate(10, 15), true);
}
public static void calculateVolume(final int[] land, final boolean showMoves)
{
final int[] water = new int[land.length];
int leftMax = 0;
int rightMax = 0;
int left = 0;
int right = land.length - 1;
int volume = 0;
while (left < right)
{
leftMax = Math.max(leftMax, land[left]);
rightMax = Math.max(rightMax, land[right]);
final boolean movedLeft;
if (leftMax >= rightMax)
{
volume += rightMax - land[right];
water[right] = (rightMax - land[right]);
right--;
movedLeft = false;
}
else
{
volume += leftMax - land[left];
water[left] = (leftMax - land[left]);
left++;
movedLeft = true;
}
if (showMoves || left == right)
{
// Render the state
render(land, water, left, right, volume, movedLeft);
// Wait until rendering the next frame
if (left != right)
{
try
{
Thread.sleep(PER_MOVE_SLEEP);
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
}
}
}
}
/**
* Generate a topography
*
* @param maxHeight
* maximum height
* @param width
* width
*
* @return
*/
private static int[] generate(int maxHeight, int width)
{
final Random random = new Random();
int[] land = new int[width];
for (int i = 0; i < land.length; i++)
land[i] = random.nextInt(maxHeight);
return land;
}
private static int max(final int... land)
{
int max = land[0];
for (int level : land)
max = Math.max(max, level);
return max;
}
/**
* Render the map, showing the land - and on top of it, the computed water thus far (and also showing the left and right
* pointers)
*
* @param land
* the land height
* @param water
* the water depths computed so far
* @param left
* the left pointer position
* @param right
* the right pointer position
* @param volume
* the current total volume
* @param movedLeft
* true if we have just moved the left pointer (false if we've just moved the right)
*/
public static void render(final int[] land,
final int[] water,
final int left,
final int right,
final int volume,
final boolean movedLeft)
{
// Compute max land level so we don't waste space rendering open air
final int max = max(land);
// Allocate enough space for the output
final StringBuilder sb = new StringBuilder((3 + max) * 3 * land.length);
// Render the pointer position (and indicate movement)
for (int i = 0; i < land.length; i++)
{
if (i == left)
sb.append(movedLeft ? " > " : " . ");
if (i == right)
{
sb.append(!movedLeft ? " < " : " . ").append('\n');
break;
}
else
sb.append(" ");
}
// Now render the map
for (int altitude = (max + 1); altitude > 0; altitude--)
{
for (int j = 0; j < land.length; j++)
{
final int groundLevel = land[j];
if (altitude - 1 == groundLevel)
if (water[j] != 0)
sb.append("~_~"); // Wet ground
else
sb.append(" _ "); // The ground
else if (groundLevel > altitude)
sb.append("| |"); // Below the ground
else if (groundLevel == altitude)
{
sb.append("|" + altitude + "|"); // Just beneath the ground
}
else if (groundLevel + water[j] >= altitude)
sb.append("~~~"); // Underwater
else
sb.append(" ");
}
sb.append('\n');
}
sb.append("Current Volume: ").append(volume).append("\n");
System.out.println(sb);
}
}