Advent Of Code 2022 - Day 08

Dec 08, 2022

It's December. Time for snow, slippery roads, hot chocolate and cozy fire places. Also time for Advent of Code. An advent calendar with small, daily programming puzzles, growing progressively more difficult.

Every year I participate in a programming language I did not use for Advent of Code before, in order to learn new ways of doing things and to challenge myself. This year, that language is F#.

Day 08: Treetop Tree House

Summary: Given the heights of trees in a grid, determine how many trees are visible from outside the grid.

A tree is visible if only smaller trees have been seen. Note that some trees can be seen from more than one side and that all trees on the edges of the grid are visible as no larger trees block them.

Example input:

30373
25512
65332
33549
35390

Read the full description here.

This is one of those problems where my imperative mind is a lot faster than my functional mind, so pay no heed to the quality of the code. It's bad. But it solves the problem.

For this problem we need to look at every line from two directions. That means we look at every cell of the grid four times.

For each pass, a tree is visible if it's strictly taller than any tree we've seen in the line so far, or if it's already been marked visible from another side.

After the more involved parsing of the past few days, parsing today's input is very easy:

let treeMap =
    input
    |> List.map (Seq.map Int32.parseChr)
    |> array2D

Int32.parseChr is a helper that wraps Int32.Parse. This gives us a mutable two-dimensional array of tree heights.

We also need a mutable two-dimensional array to mark if trees are visible. We can build it off the treeMap. Edges are always visible. We'll mark everything else as not visible to start with.

let visible = Array2D.init rows columns
                            (fun r c ->
                                match (r, c) with
                                | (0, _) -> true
                                | (_, 0) -> true
                                | (row, _) when row = rows - 1 -> true
                                | (_, col) when col = columns - 1 -> true
                                | _ -> false)

As described above, a tree is visible if it's strictly higher than a tree we've seen before in the same line and direction, or if it's already been marked visible. The argument high in this helper represents the highest tree seen so far.

let isVisible (row, col)  high =
    visible[row,col] || 
    treeMap[row,col] > high

Then comes the ugliness. First we look at each row. For each row we keep track of the highest tree and update the visible array. We have already marked the edges so we can skip them.

We have to check both directions so after going from left to right, we also go right to left.

for row = 1 to rows - 2 do
    let mutable high = treeMap[row,0]
    for left = 1 to columns - 2 do
        visible[row,left] <- isVisible (row, left) high
        high <- max high treeMap[row,left]

    high <- treeMap[row, columns - 1]
    for right = columns - 2 downto 1 do
        visible[row,right] <- isVisible (row, right) high
        high <- max high treeMap[row,right]

Then we can do the same for columns rather than rows to finish checking all four directions.

Array2D implements IEnumerable, so we can use Seq functions on them. That makes it easy to count the visible trees using my countWhere helper.

visible
|> Seq.cast
|> Seq.countWhere id

Part 2

Summary: In part two we have to determine the tree with the highest scenic score. The scenic score of a tree is determined by how many other trees are visible from that tree. Once again we have to look in all four cardinal directions.

The amount of trees per cardinal direction are then multiplied by each other to determine the score.

So if to the north we can see 3 trees, to the south we can see 2 trees and to the east and west we can see 1 tree, the score is 3 * 2 * 1 * 1 = 6

A tree is visible from another tree if it's smaller than the tree of origin and the view has not been obstructed yet by a tree that is equal to or larger than the tree of origin.

The way I solved this is… not pretty. The helper function scenicScore takes a grid location and the height of the tree at that location and returns the score for that tree.

It does that by looking into each direction and, if not yet blocked, adding 1 to the score for that line. Note that the tree that does the blocking can be seen.

let scenicScore row col treeHeight =
    let mutable score = 1
    let mutable visible = 0
    let mutable blocked = false
    for r = row - 1 downto 0 do
        if treeMap[r,col] >= treeHeight && not blocked then visible <- visible + 1; blocked <- true
        if not blocked then visible <- visible + 1
    score <- score * visible
    visible <- 0
    blocked <- false
    for r = row + 1 to rows - 1 do
        if treeMap[r,col] >= treeHeight && not blocked then visible <- visible + 1; blocked <- true
        if not blocked then visible <- visible + 1

For two directions, this is what that looks like. I'll leave the second dimension of this monstrosity as an exercise to the reader. It involves some copying and pasting.

Applying this function to every location in the treeMap and finding the maximum can be done elegantly again.

treeMap
|> Array2D.mapi scenicScore
|> Seq.cast<int>
|> Seq.max

Improvements

There is a lot of room for improvement here, but due to other commitments there was no time to attempt those improvements. I'll update this post later with improvements.

Reflection

This is one of those problems where my imperative mind is a lot faster than my functional mind. I spent about 30 minutes solving the problem in imperative F#. I spent quite a some time trying to make it nicer but failed. Other commitments prevented me from spending more time.

Since there's much to learn here I really do want to try and make it nicer. It'll have to happen later, though.

The good news is that this is, I think, the first time I've used an unfamiliar language for a grid-based problem and finished the problem in a reasonable amount of time.

It's not good code, but it solves the problem and earns the stars and was easy to write in a short amount of time. Not all bad, but it definitely needs some work.

The full code for the day is on GitHub.