Task 18

Thorn Thaler - <

2025-11-07

1 Setup

1.1 Libraries

library(httr)
library(xml2)
library(magrittr)
library(dplyr)
library(purrr)
library(stringr)
library(tidyr)

1.2 Retrieve Data from AoC

session_cookie <- set_cookies(session = keyring::key_get("AoC-GitHub-Cookie"))
base_url <- paste0("https://adventofcode.com/", params$year, "/day/", params$task_nr)
puzzle <- GET(
  base_url,
  session_cookie
) %>%
  content(encoding = "UTF-8") %>%
  xml_find_all("///article") %>%
  lapply(as.character)

parse_puzzle_data <- function(text_block = readClipboard()) {
  if (length(text_block) == 1L) {
    text_block <- text_block %>%
      str_split("\n") %>%
      extract2(1L) %>%
      keep(nzchar)
  }
  text_block %>%
    str_split("") %>%
    do.call(rbind, .)
}

puzzle_data <- local({
  GET(
    paste0(base_url, "/input"),
    session_cookie
  ) %>%
    content(encoding = "UTF-8") %>%
    parse_puzzle_data()
})

2 Puzzle Day 18

2.1 Part 1

2.1.1 Description

— Day 18: Settlers of The North Pole —

On the outskirts of the North Pole base construction project, many Elves are collecting lumber.

The lumber collection area is 50 acres by 50 acres; each acre can be either open ground (.), trees (|), or a lumberyard (#). You take a scan of the area (your puzzle input).

Strange magic is at work here: each minute, the landscape looks entirely different. In exactly one minute, an open acre can fill with trees, a wooded acre can be converted to a lumberyard, or a lumberyard can be cleared to open ground (the lumber having been sent to other projects).

The change to each acre is based entirely on the contents of that acre as well as the number of open, wooded, or lumberyard acres adjacent to it at the start of each minute. Here, “adjacent” means any of the eight acres surrounding that acre. (Acres on the edges of the lumber collection area might have fewer than eight adjacent acres; the missing acres aren’t counted.)

In particular:

  • An open acre will become filled with trees if three or more adjacent acres contained trees. Otherwise, nothing happens.
  • An acre filled with trees will become a lumberyard if three or more adjacent acres were lumberyards. Otherwise, nothing happens.
  • An acre containing a lumberyard will remain a lumberyard if it was adjacent to at least one other lumberyard and at least one acre containing trees. Otherwise, it becomes open.

These changes happen across all acres simultaneously, each of them using the state of all acres at the beginning of the minute and changing to their new form by the end of that same minute. Changes that happen during the minute don’t affect each other.

For example, suppose the lumber collection area is instead only 10 by 10 acres with this initial configuration:

Initial state:
.#.#...|#.
.....#|##|
.|..|...#.
..|#.....#
#.#|||#|#|
...#.||...
.|....|...
||...#|.#|
|.||||..|.
...#.|..|.

After 1 minute:
.......##.
......|###
.|..|...#.
..|#||...#
..##||.|#|
...#||||..
||...|||..
|||||.||.|
||||||||||
....||..|.

After 2 minutes:
.......#..
......|#..
.|.|||....
..##|||..#
..###|||#|
...#|||||.
|||||||||.
||||||||||
||||||||||
.|||||||||

After 3 minutes:
.......#..
....|||#..
.|.||||...
..###|||.#
...##|||#|
.||##|||||
||||||||||
||||||||||
||||||||||
||||||||||

After 4 minutes:
.....|.#..
...||||#..
.|.#||||..
..###||||#
...###||#|
|||##|||||
||||||||||
||||||||||
||||||||||
||||||||||

After 5 minutes:
....|||#..
...||||#..
.|.##||||.
..####|||#
.|.###||#|
|||###||||
||||||||||
||||||||||
||||||||||
||||||||||

After 6 minutes:
...||||#..
...||||#..
.|.###|||.
..#.##|||#
|||#.##|#|
|||###||||
||||#|||||
||||||||||
||||||||||
||||||||||

After 7 minutes:
...||||#..
..||#|##..
.|.####||.
||#..##||#
||##.##|#|
|||####|||
|||###||||
||||||||||
||||||||||
||||||||||

After 8 minutes:
..||||##..
..|#####..
|||#####|.
||#...##|#
||##..###|
||##.###||
|||####|||
||||#|||||
||||||||||
||||||||||

After 9 minutes:
..||###...
.||#####..
||##...##.
||#....###
|##....##|
||##..###|
||######||
|||###||||
||||||||||
||||||||||

After 10 minutes:
.||##.....
||###.....
||##......
|##.....##
|##.....##
|##....##|
||##.####|
||#####|||
||||#|||||
||||||||||

After 10 minutes, there are 37 wooded acres and 31 lumberyards. Multiplying the number of wooded acres by the number of lumberyards gives the total resource value after ten minutes: 37 * 31 = 1147.

What will the total resource value of the lumber collection area be after 10 minutes?

2.1.2 Solution

We implement the first part in R which - given the small numbers is feasible.

count_non_empty <- function(n, acre = puzzle_data) {
  dirs <- rbind(
    c(-1L, -1L),
    c(-1L, 0L),
    c(-1L, 1L),
    c(0L, -1L),
    c(0L, 1L),
    c(1L, -1L),
    c(1L, 0L),
    c(1L, 1L)
  )

  get_neighbors <- function(pos) {
    nbs <- t(t(dirs) + c(pos))
    nbs[between(nbs[, 1L], 1L, nrow(acre)) &
      between(nbs[, 2L], 1L, ncol(acre)), , drop = FALSE]
  }

  change_state <- function(pos) {
    type <- acre[pos]
    nbs <- get_neighbors(pos)
    if (type == ".") {
      ## open field
      cnt <- sum(acre[nbs] == "|")
      if_else(cnt >= 3L, "|", ".")
    } else if (type == "|") {
      ## filled with trees
      cnt <- sum(acre[nbs] == "#")
      if_else(cnt >= 3L, "#", "|")
    } else if (type == "#") {
      ## lumberyard
      if_else(sum(acre[nbs] == "#") >= 1 &&
        sum(acre[nbs] == "|") >= 1, "#", ".")
    }
  }

  for (min in 1:n) {
    new <- acre
    for (i in seq_len(nrow(acre))) {
      for (j in seq_len(ncol(acre))) {
        new[i, j] <- change_state(cbind(i, j))
      }
    }
    acre <- new
  }
  sum(acre == "|") * sum(acre == "#")
}
count_non_empty(10, puzzle_data)
## [1] 644640

2.2 Part 2

2.2.1 Description

— Part Two —

This important natural resource will need to last for at least thousands of years. Are the Elves collecting this lumber sustainably?

What will the total resource value of the lumber collection area be after 1000000000 minutes?

2.2.2 Solution

For the second part we re-implement the algorithm in C++ to be faster. We use a hash to detect the cycle and once we detect it we fast forward.

#ifndef STANDALONE
#include <Rcpp.h>
using namespace Rcpp;
#else
#include <iostream>
#endif
#include <unordered_map>
#include <utility>
#include <vector>

int count_non_empty(long n, std::vector<std::string>& acre) {

  int nrow = acre.size();
  int ncol = acre[0].size();

  std::vector<std::pair<int, int>> dirs = {
      {-1, -1}, {-1, 0}, {-1, 1}, {0, -1}, {0, 1}, {1, -1}, {1, 0}, {1, 1}};

  auto count_neighbors = [&](int i, int j, const std::vector<std::string>& grid, char type) {
    int cnt = 0;
    for (auto& d : dirs) {
      int ni = i + d.first;
      int nj = j + d.second;
      if (ni >= 0 && ni < nrow && nj >= 0 && nj < ncol) {
        if (grid[ni][nj] == type)
          cnt++;
      }
    }
    return cnt;
  };

  auto make_key = [&](const std::vector<std::string>& grid) {
    std::string key;
    for (const auto& row : grid) {
      key += row;
    }
    return key;
  };

  std::unordered_map<std::string, long> seen_index;
  std::vector<std::string> states;

  std::string key = make_key(acre);
  seen_index[key] = 0;
  states.push_back(key);
  for (long minute = 1; minute <= n; ++minute) {
    std::vector<std::string> next = acre;

    for (int i = 0; i < nrow; i++) {
      for (int j = 0; j < ncol; j++) {
        char type = acre[i][j];

        if (type == '.') {
          int cnt = count_neighbors(i, j, acre, '|');
          next[i][j] = (cnt >= 3) ? '|' : '.';
        } else if (type == '|') {
          int cnt = count_neighbors(i, j, acre, '#');
          next[i][j] = (cnt >= 3) ? '#' : '|';
        } else if (type == '#') {
          int cnt_hash = count_neighbors(i, j, acre, '#');
          int cnt_tree = count_neighbors(i, j, acre, '|');
          next[i][j] = (cnt_hash >= 1 && cnt_tree >= 1) ? '#' : '.';
        }
      }
    }
    key = make_key(next);

    if (seen_index.find(key) != seen_index.end()) {
      long first_seen = seen_index[key];
      long cycle_length = minute - first_seen;

      long remaining = n - minute;
      long target_index = first_seen + ((n - first_seen) % cycle_length);

      std::string target_key = states[target_index];
      int pos = 0;
      for (int i = 0; i < nrow; i++) {
        for (int j = 0; j < ncol; j++) {
          acre[i][j] = target_key[pos++];
        }
      }
      break;
    } else {
      seen_index[key] = minute;
      states.push_back(key);
      acre = next;
    }
  }

  int cnt_tree = 0, cnt_lumber = 0;
  for (int i = 0; i < nrow; i++) {
    for (int j = 0; j < ncol; j++) {
      char type = acre[i][j];
      if (type == '|') {
        cnt_tree++;
      } else if (type == '#') {
        cnt_lumber++;
      }
    }
  }

  return cnt_tree * cnt_lumber;
}
#ifndef STANDALONE
// [[Rcpp::export]]
int count_non_empty_fast(long n, CharacterMatrix acre) {
  int nrow = acre.nrow();
  int ncol = acre.ncol();

  std::vector<std::string> acre_vec(nrow);
  for (int i = 0; i < nrow; i++) {
    std::string row_str;
    for (int j = 0; j < ncol; j++) {
      row_str += Rcpp::as<std::string>(acre(i, j));
    }
    acre_vec[i] = row_str;
  }

  return count_non_empty(n, acre_vec);
}

#else
int main() {
  std::vector<std::string> acre = {".#.#...|#.",
                                   ".....#|##|",
                                   ".|..|...#.",
                                   "..|#.....#",
                                   "#.#|||#|#|",
                                   "...#.||...",
                                   ".|....|...",
                                   "||...#|.#|",
                                   "|.||||..|.",
                                   "...#.|..|."};
  std::cout << count_non_empty(10, acre) << std::endl;
  return 0;
}
#endif
count_non_empty_fast(1000000000, puzzle_data)
## [1] 191080