Part two of day 9 had me brute force a flood fill for the first time in my programming career. I'm sure it's possible to do it more efficiently than I did, as I was definitely iterating over the same spots too many times, but, for expediency, I did what I knew would work rather than getting fancier.
I represented the given data in a class I called HeightMap. I thought it would make my life easier if a grew the map by a unit in every direction, so that I could work inside the perimeter without worrying about special casing for the edges. (Not sure that really paid off, but neither do I think it really hurt any.) It didn't start out storing the locations for part one, because of course the part one answer didn't call for them. When I realized I would need them for part two, I had to quickly refactor my part one to start using a Point record and store all the Points from part one to use in part two. The HeightMap class has all the logic to find the answers, but I ended up including a sort() in the method for part two.
Here's the part one and part two code, including the aforementioned sort(). I don't think there is anything here to bother spoiler blurring:
I represented the given data in a class I called HeightMap. I thought it would make my life easier if a grew the map by a unit in every direction, so that I could work inside the perimeter without worrying about special casing for the edges. (Not sure that really paid off, but neither do I think it really hurt any.) It didn't start out storing the locations for part one, because of course the part one answer didn't call for them. When I realized I would need them for part two, I had to quickly refactor my part one to start using a Point record and store all the Points from part one to use in part two. The HeightMap class has all the logic to find the answers, but I ended up including a sort() in the method for part two.
Java:
final class HeightMap
{
final int[][] heights;
final int width;
final int height;
HeightMap(final String[] mapHeights)
{
width = mapHeights[0].length();
height = mapHeights.length;
heights = new int[width+2][height+2];
for (int x=0; x<=width+1; x++)
{
for (int y=0; y<=height+1; y++)
{
heights[x][y] = -1;
}
}
for (int x=1; x<=width; x++)
{
for (int y=1; y<=height; y++)
{
heights[x][y] = mapHeights[y-1].charAt(x-1)-'0';
}
}
}
int getRiskLevel()
{
int totalRiskLevel = 0;
final Point[] allLowPoints = getAllLowPoints();
for (final Point p : allLowPoints)
{
totalRiskLevel += 1+heights[p.x][p.y];
}
return totalRiskLevel;
}
@org.eclipse.jdt.annotation.NonNullByDefault({})
record Point(int x, int y) {}
Point[] getAllLowPoints()
{
final List<Point> lowPoints = new ArrayList<>();
for (int x=1; x<=width; x++)
{
for (int y=1; y<=height; y++)
{
if (isLowPoint(new Point(x,y)))
{
lowPoints.add(new Point(x,y));
}
}
}
return lowPoints.toArray(new Point[lowPoints.size()]);
}
int[] getSizesOfAllBasins()
{
final List<Integer> basinSizes = new ArrayList<>();
for (final Point p : getAllLowPoints())
{
final int basinSize = getBasinSize(p);
basinSizes.add(basinSize);
}
final int[] result = new int[basinSizes.size()];
for (int i=0; i<basinSizes.size(); i++) result[i] = basinSizes.get(i);
return result;
}
private int getBasinSize(final Point startingPoint)
{
final Set<Point> pointsInBasin = new HashSet<>();
pointsInBasin.add(startingPoint);
growBasin(pointsInBasin);
return pointsInBasin.size();
}
private void growBasin(final Set<Point> pointsInBasin)
{
final Set<Point> pointsToAdd = new HashSet<>();
for (final Point p : pointsInBasin)
{
final Point[] adjacentPoints = getAdjacentPoints(p);
for (final Point adjacentPoint : adjacentPoints)
{
if (!pointsInBasin.contains(adjacentPoint))
{
if (heights[adjacentPoint.x][adjacentPoint.y] < 9)
{
pointsToAdd.add(adjacentPoint);
}
}
}
}
if (!pointsToAdd.isEmpty())
{
pointsInBasin.addAll(pointsToAdd);
growBasin(pointsInBasin); // recursively check the newly added points
// (inefficient because it will recheck already checked points too)
}
}
private boolean isLowPoint(final Point point)
{
final Point[] adjacentPoints = getAdjacentPoints(point);
final int c = heights[point.x][point.y];
boolean allGreater = true;
for (final Point otherPoint : adjacentPoints)
{
if (heights[otherPoint.x][otherPoint.y] <= c)
{
allGreater = false;
break;
}
}
return allGreater;
}
private Point[] getAdjacentPoints(final Point p)
{
final Set<Point> adjacentPoints = new HashSet<>();
int t = heights[p.x][p.y-1];
if (t >=0) adjacentPoints.add(new Point(p.x, p.y-1));
int l = heights[p.x-1][p.y];
if (l >=0) adjacentPoints.add(new Point(p.x-1, p.y));
int r = heights[p.x+1][p.y];
if (r >=0) adjacentPoints.add(new Point(p.x+1, p.y));
int b = heights[p.x][p.y+1];
if (b >=0) adjacentPoints.add(new Point(p.x, p.y+1));
return adjacentPoints.toArray(new Point[adjacentPoints.size()]);
}
@Override
public String toString()
{
final StringBuilder sb = new StringBuilder();
for (int y=0; y<=height+1; y++)
{
for (int x=0; x<=width+1; x++)
{
if (heights[x][y] < 0)
sb.append('X');
else
sb.append((char)('0'+heights[x][y]));
}
sb.append('\n');
}
return sb.toString();
}
}
Here's the part one and part two code, including the aforementioned sort(). I don't think there is anything here to bother spoiler blurring:
Java:
public static long getPart1Answer(final String day09Input, final String[] day09InputLines)
{
final HeightMap hm = new HeightMap(day09InputLines);
return hm.getRiskLevel();
}
public static long getPart2Answer(final String day09Input, final String[] day09InputLines)
{
final HeightMap hm = new HeightMap(day09InputLines);
final int[] basinSizes = hm.getSizesOfAllBasins();
Arrays.sort(basinSizes);
return (long)basinSizes[basinSizes.length-1] *
(long)basinSizes[basinSizes.length-2] *
(long)basinSizes[basinSizes.length-3];
}