Advent of Code Day12: Garden Groups

Today’s challenge (1)https://adventofcode.com/2024/day/12 felt very strange to me. I read the question. I knew what I had to do for Part 1, but I didn’t feel very motivated to actually finish my implementation. I pushed through and eventually got it done, then spent too long thinking about how to do Part 2 before I realised that it was more or less the same approach as for part 1, just with different parameters.

Part 1 #

Given a grid of a farm and its crops we are supposed to work out some number based on the area and the perimeter.

The approach I used was that of flood filling. I take a point from the graph and do a search for all its neighbours that have the same crop type, and I keep doing that until I have found all connected plots of the same type. I keep track of the plots that I have seen so I don’t double count them, and do this for all the plots.

Counting the number of plots in each region gives me the area.

Since I was using GameplayKit to help me with my graph, I went through and removed all edges that weren’t connected to a plot of the same type. For each plot I then work out the number of sides by subtracting the number of graph edges it has to other plots from 4. Then multiply and sum to get the first answer.

extension Day12 {
  typealias GridGraph = GKGridGraph<GKGridGraphNode>
  typealias Node = GKGridGraphNode

  func farm(from rows: [[Character]]) -> GridGraph {
    let width = Int32(rows[0].count)
    let height = Int32(rows.count)
    let origin = vector_int2(0, 0)
    let graph = GKGridGraph(
      fromGridStartingAt: origin,
      width: width,
      height: height,
      diagonalsAllowed: false,
      nodeClass: Node.self
    )

    for node in graph.nodes! {
      let node = node as! Node
      let position = node.gridPosition
      let (row, column) = (Int(position.y), Int(position.x))

      for neighbor in node.connectedNodes {
        let neighbor = neighbor as! Node
        let nPosition = neighbor.gridPosition
        let (nRow, nColumn) = (Int(nPosition.y), Int(nPosition.x))

        if rows[nRow][nColumn] != rows[row][column] {
          node.removeConnections(to: [neighbor], bidirectional: true)
        }
      }
    }

    return graph
  }

  func regions(from graph: GridGraph, rows _: [[Character]]) -> [Set<Node>] {
    var regions: [Set<Node>] = []
    var seen: Set<Node> = []

    for node in graph.nodes! {
      let node = node as! Node
      guard !seen.contains(node) else { continue }

      var stack = [node]
      var currentRegion = Set<Node>()

      while !stack.isEmpty {
        let currentNode = stack.removeLast()
        guard !seen.contains(currentNode) else { continue }
        seen.insert(currentNode)
        currentRegion.insert(currentNode)

        // Add unvisited neighbors of the same region to the stack
        for neighbor in currentNode.connectedNodes {
          let neighbor = neighbor as! Node
          if !seen.contains(neighbor) {
            stack.append(neighbor)
          }
        }
      }

      if !currentRegion.isEmpty {
        regions.append(currentRegion)
      }
    }

    return regions
  }

  func price(_ region: Set<Node>) -> Int {
    let area = region.count
    let perimeter = region.reduce(0) { partialResult, node in
      partialResult + 4 - node.connectedNodes.count
    }

    return area * perimeter
  }
}

Part 2 #

This took a lot more thought before I bit the bullet and wrote the code.

I defined a struct to represent and edge for a plot:

struct Edge: Hashable {
   enum Direction: Hashable {
     case top, right, bottom, left
   }

   let position: vector_int2
   let direction: Direction

   var neighbours: [Edge] {
     let x = position.x
     let y = position.y
     switch direction {
     case .top, .bottom:
       return [
         Edge(position: vector_int2(x: x + 1, y: y), direction: direction),
         Edge(position: vector_int2(x: x - 1, y: y), direction: direction),
       ]
     case .right, .left:
       return [
         Edge(position: vector_int2(x: x, y: y + 1), direction: direction),
         Edge(position: vector_int2(x: x, y: y - 1), direction: direction),
       ]
     }
   }
 }

This also gives me the neighbours I expect to have in horizontal and vertical directions.

I already have a function for working out a connected region, and I use that to generate all the plot edges:

func edges(for region: Set<Node>) -> Set<Edge> {
  var edges: Set<Edge> = []

  for node in region {
    let position = node.gridPosition
    let above = position.above
    let below = position.below
    let left = position.left
    let right = position.right

    let neighbours = node.connectedNodes.map { $0 as! Node }.map(\.gridPosition)
    if !neighbours.contains(above) {
      edges.insert(Edge(position: position, direction: .top))
    }

    if !neighbours.contains(below) {
      edges.insert(Edge(position: position, direction: .bottom))
    }

    if !neighbours.contains(left) {
      edges.insert(Edge(position: position, direction: .left))
    }

    if !neighbours.contains(right) {
      edges.insert(Edge(position: position, direction: .right))
    }
  }
  return edges
}

Now I use the same flood filling to find all the connected edges. I take an edge off the list, and generate it’s expected neighbours and count them up.

func sides(for region: Set<Node>) -> Int {
  let edges = edges(for: region)
  var totalSides = 0
  var seen = Set<Edge>()

  for edge in edges {
    guard !seen.contains(edge) else { continue }
    var stack = Deque<Edge>([edge])

    while !stack.isEmpty {
      let current = stack.removeFirst()
      guard !seen.contains(current) else { continue }
      seen.insert(current)

      for neighbour in current.neighbours {
        guard !seen.contains(neighbour) else { continue }
        if edges.contains(neighbour) {
          stack.append(neighbour)
        }
      }
    }

    totalSides += 1
  }
  return totalSides
}

And that gave me the correct answer.

As usual, the full code for this is on Github (2)https://github.com/Abizern/aoc-swift-2024/blob/main/Sources/Day12.swift . It felt like a slog, I don’t mind telling you.

>