1 Setup
1.1 Libraries
library(httr)
library(xml2)
library(magrittr)
library(tibble)
library(dplyr)
library(purrr)
library(stringr)
library(igraph)
1.2 Retrieve Data from AoC
session_cookie <- set_cookies(session = keyring::key_get("AoC-GitHub-Cookie"))
base_url <- paste0("https://adventofcode.com/2021/day/", params$task_nr)
puzzle <- GET(base_url,
session_cookie) %>%
content(encoding = "UTF-8") %>%
xml_find_all("///article") %>%
lapply(as.character)
puzzle_data <- local({
res <- GET(paste0(base_url, "/input"),
session_cookie) %>%
content(encoding = "UTF-8") %>%
str_split("\n+") %>%
`[[`(1L)
rules <- head(tail(res, -1L), -1L)
lkp <- str_extract(rules, ".$") %>%
set_names(str_extract(rules, "^.."))
list(start = res[[1L]],
rules = lkp)
})
2 Puzzle Day 14
2.1 Part 1
2.1.1 Description
— Day 14: Extended Polymerization —
The incredible pressures at this depth are starting to put a strain on your submarine. The submarine has polymerization equipment that would produce suitable materials to reinforce the submarine, and the nearby volcanically-active caves should even have the necessary input elements in sufficient quantities.
The submarine manual contains instructions for finding the optimal polymer formula; specifically, it offers a polymer template and a list of pair insertion rules (your puzzle input). You just need to work out what polymer would result after repeating the pair insertion process a few times.
For example:
NNCB
CH -> B
HH -> N
CB -> H
NH -> C
HB -> C
HC -> B
HN -> C
NN -> C
BH -> H
NC -> B
NB -> B
BN -> B
BB -> N
BC -> B
CC -> N
CN -> C
The first line is the polymer template - this is the starting point of the process.
The following section defines the pair insertion rules. A rule like AB -> C
means that when elements A
and B
are immediately adjacent, element C
should be inserted between them. These insertions all happen simultaneously.
So, starting with the polymer template NNCB
, the first step simultaneously considers all three pairs:
-
The first pair (
NN
) matches the ruleNN -> C
, so elementC
is inserted between the firstN
and the secondN
. -
The second pair (
NC
) matches the ruleNC -> B
, so elementB
is inserted between theN
and theC
. -
The third pair (
CB
) matches the ruleCB -> H
, so elementH
is inserted between theC
and theB
.
Note that these pairs overlap: the second element of one pair is the first element of the next pair. Also, because all pairs are considered simultaneously, inserted elements are not considered to be part of a pair until the next step.
After the first step of this process, the polymer becomes NCNBCHB
.
Here are the results of a few steps using the above rules:
Template: NNCB
After step 1: NCNBCHB
After step 2: NBCCNBBBCBHCB
After step 3: NBBBCNCCNBBNBNBBCHBHHBCHB
After step 4: NBBNBNBBCCNBCNCCNBBNBBNBBBNBBNBBCBHCBHHNHCBBCBHCB
This polymer grows quickly. After step 5, it has length 97; After step 10, it has length 3073. After step 10, B
occurs 1749 times, C
occurs 298 times, H
occurs 161 times, and N
occurs 865 times; taking the quantity of the most common element (B
, 1749) and subtracting the quantity of the least common element (H
, 161) produces 1749 - 161 = 1588
.
Apply 10 steps of pair insertion to the polymer template and find the most and least common elements in the result. What do you get if you take the quantity of the most common element and subtract the quantity of the least common element?
2.1.2 Solution
We split the string in pairs of two and replace matches according to the given rules. Then we drop the first letter in each pair or triplet, as it is already present in the pair or triplet before.
format_rule <- function(rule) {
sprintf("%s -> %s", names(rule), rule)
}
insert <- function(rule) {
str_c(str_sub(names(rule), 1L, 1L),
rule,
str_sub(names(rule), 2L, 2L))
}
replace_line <- function(n,
str = puzzle_data$start,
rules = puzzle_data$rules) {
wrapper <- function(str, i) {
idx <- seq(1L, str_length(str) - 1L)
tokens <- str_sub(str, idx, idx + 1L)
tokens <- insert(rules[tokens])
tokens[-1L] <- str_sub(tokens[-1L], 2L)
str_c(tokens, collapse = "")
}
all_ltrs <- str_c(rules, collapse = "") %>%
str_split("") %>%
`[[`(1) %>%
unique()
reduce(seq(1L, n), wrapper, .init = str) %>%
str_count(fixed(all_ltrs)) %>%
range() %>%
diff()
}
replace_line(10)
## [1] 3408
2.2 Part 2
2.2.1 Description
— Part Two —
The resulting polymer isn’t nearly strong enough to reinforce the submarine. You’ll need to run more steps of the pair insertion process; a total of 40 steps should do it.
In the above example, the most common element is B
(occurring 2192039569602
times) and the least common element is H
(occurring 3849876073
times); subtracting these produces 2188189693529
.
Apply 40 steps of pair insertion to the polymer template and find the most and least common elements in the result. What do you get if you take the quantity of the most common element and subtract the quantity of the least common element?
2.2.2 Solution
While the previous brute-force solution works for small n
it quickly becomes not
manageable, because at each step the string can potentially grow by
100 (=: k
) characters. That is, while the upper bound for the
string length after 10 iterations is k ^ 10 =
10^{20}
,
the upper bound for 40 iterations is already
r length(puzzle_data$rules) ^ 40`
. Of course not all 100
will hit in each of the iterations, hence we manage with 10 iterations but clearly for 40
iterations we have to find a smarter algorithm.
We will resort to a directed graph. Each node represents a tuple like
HF
. By applying rule
HF -> O
this tuple produces
HOF
, which eventually results in the two new tuples
HO, OF
. In the graph we hence connect
HF
with
HO, OF
via a directed edge. In this
way we connect all tuples to its 2 resulting tuples to form a graph. We connect a
dedicated start
node to the tuples found in the start word. We assign an initial load
of 1
to the start node. If a tuple appears k
times in the start string, there
will be k
edges from the start node to this very tuple.
Then the algorithm works as follows:
- For each outgoing edge, add the full load to the child node. That is, if there are two
nodes
P
andP'
say, which have an edge intoC
, then we add the load ofP
plus the load ofP'
to the load ofC
. Note, that the graph may have some loops (a rule likeKN -> K
will lead to such a loop). This reflects that each tuple when polymerized will create a new tuple. If there are for instance 3KN -> H
nodes and 2NN -> H
nodes we will - inter alia - have3 + 2 = 5
newHN
nodes. The amount is reflectd by theload
. - We also have to reduce the number of “mother” polymers accordingly. In the above
example we will reduce the load of
KN
andNN
by 3 and 2 respectively. Please note, that in one run we can produce and destroy a certain tuple. - Once we transferred the loads to the childs, we recurse into all childs and continue until we reached the desired depth.
get_neighbor_node <- function(tuple) {
nbs <- insert(tuple)
from <- rep(1:2, length(tuple))
str_sub(rep(nbs, each = 2), from, from + 1)
}
create_graph_from_rules <- function(start = puzzle_data$start,
rules = puzzle_data$rules) {
gr_dat <- data.frame(from = rep(names(rules), each = 2L),
to = get_neighbor_node(rules),
insert = rep(rules, each = 2L),
visited = 0L,
load = 0L)
G <- gr_dat %>%
select(1:2) %>%
graph_from_data_frame(vertices = gr_dat %>%
select(-to) %>% unique())
V(G)$color <- "orange"
V(G)$shape <- "circle"
idx <- seq_len(str_length(start) - 1L)
new_edges <- rep(str_sub(start, idx, idx + 1L), each = 2L)
new_edges[2L * (0:(length(new_edges) / 2L - 1L)) + 1L] <- "start"
G +
vertices("start",
color = "steelblue",
shape = "square",
load = 1L,
visited = 1L) +
edges(new_edges)
}
walk_graph <- function(max_depth,
start = puzzle_data$start,
rules = puzzle_data$rules) {
op <- options(scipen = 99)
on.exit(options(op))
G <- create_graph_from_rules(start, rules)
start_cnt <- tibble(ltr = unique(str_split(start, "")[[1L]]),
n = str_count(start, ltr))
do_walk <- function(nodes, depth) {
if (depth == max_depth) {
nodes <- difference(V(G), V(G)["start", visited == 0L])
tibble(ltr = nodes$insert,
n = nodes$visited) %>%
group_by(ltr) %>%
summarize(n = sum(n)) %>%
full_join(start_cnt,
by = "ltr") %>%
transmute(ltr,
n = coalesce(n.x, 0L) + coalesce(n.y, 0L)) %>%
pull(n) %>%
range() %>%
diff()
} else {
incs <- incident_edges(G, nodes) %>%
unlist()
nbs <- adjacent_vertices(G, nodes) %>%
unlist() %>%
unique()
src_dest <- tibble(src = tail_of(G, incs),
dest = head_of(G, incs),
load = V(G)[src]$load)
dest <- src_dest %>%
group_by(dest) %>%
summarize(load = sum(load))
V(G)[dest$dest]$load <<- V(G)[dest$dest]$load + dest$load
src <- src_dest %>%
distinct(src, load)
V(G)[src$src]$load <<- V(G)[src$src]$load - src$load
V(G)[nbs]$visited <<- V(G)[nbs]$visited + V(G)[nbs]$load
Recall(V(G)[nbs], depth + 1L)
}
}
do_walk(V(G)["start"], 0L)
}
options(scipen = 1)
walk_graph(40)
[1] 3724343376942