1 Setup
1.1 Libraries
library(httr)
library(xml2)
library(magrittr)
library(dplyr)
library(purrr)
library(stringr)
library(knitr)
library(cli)
library(bit64)
1.2 Retrieve Data from AoC
session_cookie <- set_cookies(session = keyring::key_get("AoC-GitHub-Cookie"))
base_url <- paste0("https://adventofcode.com/2024/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()) {
formulas <- text_block %>%
unlist() %>%
keep(nzchar)
res <- str_extract(formulas, "^\\d+") %>%
as.integer64() %>%
as.list()
ops <- str_remove(formulas, "^\\d+: *") %>%
str_split(fixed(" ")) %>%
map(as.integer64)
map2(ops, res, ~ list(ops = .x, res = .y))
}
puzzle_data <- local({
GET(paste0(base_url, "/input"),
session_cookie) %>%
content(encoding = "UTF-8") %>%
str_split("\n") %>%
parse_puzzle_data()
})
test_input <- parse_puzzle_data()
2 Puzzle Day 7
2.1 Part 1
2.1.1 Description
— Day 7: Bridge Repair —
The Historians take you to a familiar rope bridge over a river in the middle of a jungle. The Chief isn’t on this side of the bridge, though; maybe he’s on the other side?
When you go to cross the bridge, you notice a group of engineers trying to repair it. (Apparently, it breaks pretty frequently.) You won’t be able to cross until it’s fixed.
You ask how long it’ll take; the engineers tell you that it only needs final calibrations, but some young elephants were playing nearby and stole all the operators from their calibration equations! They could finish the calibrations if only someone could determine which test values could possibly be produced by placing any combination of operators into their calibration equations (your puzzle input).
For example:
190: 10 19
3267: 81 40 27
83: 17 5
156: 15 6
7290: 6 8 6 15
161011: 16 10 13
192: 17 8 14
21037: 9 7 18 13
292: 11 6 16 20
Each line represents a single equation. The test value appears before the colon on each line; it is your job to determine whether the remaining numbers can be combined with operators to produce the test value.
Operators are always evaluated left-to-right, not according to precedence rules. Furthermore, numbers in the equations cannot be rearranged. Glancing into the jungle, you can see elephants holding two different types of operators: add (+
) and multiply (*
).
Only three of the above equations can be made true by inserting operators:
-
190: 10 19
has only one position that accepts an operator: between10
and19
. Choosing+
would give29
, but choosingwould give the test value (
10 19 = 190
). -
3267: 81 40 27
has two positions for operators. Of the four possible configurations of the operators, two cause the right side to match the test value:81 + 40 * 27
and81 * 40 + 27
both equal3267
(when evaluated left-to-right)! -
292: 11 6 16 20
can be solved in exactly one way:11 + 6 * 16 + 20
.
The engineers just need the total calibration result, which is the sum of the test values from just the equations that could possibly be true. In the above example, the sum of the test values for the three equations listed above is 3749
.
Determine which equations could possibly be true. What is their total calibration result?
2.1.2 Solution
We use a backtracking algorithm with all the operators from the right. If the current result is divisible without remainder by the current operator, we recurse into a division branch with the current result divided by the current operator. If the current result is greater than the current operator we recurse into a subtraction branch with the current result minus the current operator. If no numbers are left, we check whether we end up at zero for a subtraction branch or at one for a division branch. We stop early if at any recursion neither of the 2 pre-conditions (no remainder and greater result) holds true.
The benefit from doing it from the right is that we can branch a lot of cases where the division would not work. A brute force method would need the following amount of recursions:
puzzle_data %>%
map_dbl(~ 2 ^ (.x$ops %>% length())) %>%
sum()
## [1] 589424
while the branching brought that number down to 30,954.
nr_calls <- 0
is_valid1 <- function(res, operands, i, division = FALSE) {
nr_calls <<- nr_calls + 1L
ret <- FALSE
if (i == 0L) {
ret <- (division && res == 1L) || (!division && res == 0L)
} else {
operand <- operands[i]
if ((res - operand) >= 0L) {
if (Recall(res - operand, operands, i - 1L, FALSE)) {
ret <- TRUE
}
}
if (res %% operand == 0L) {
if (Recall(res / operand, operands, i - 1L, TRUE)) {
ret <- TRUE
}
}
}
ret
}
get_valid <- function(calibrations, validator) {
nr_calls <<- 0
map_lgl(calibrations, function(calibration) {
ops <- calibration$ops
res <- calibration$res
validator(res, ops, length(ops))
})
}
idx1 <- get_valid(puzzle_data, is_valid1)
res1 <- puzzle_data[idx1] %>%
map("res") %>%
do.call(c, .) %>%
sum()
res1
## integer64
## [1] 882304362421
2.2 Part 2
2.2.1 Description
— Part Two —
The engineers seem concerned; the total calibration result you gave them is nowhere close to being within safety tolerances. Just then, you spot your mistake: some well-hidden elephants are holding a third type of operator.
The concatenation operator (||
) combines the digits from its left and right inputs into a single number. For example, 12 || 345
would become 12345
. All operators are still evaluated left-to-right.
Now, apart from the three equations that could be made true using only addition and multiplication, the above example has three more equations that can be made true by inserting operators:
-
156: 15 6
can be made true through a single concatenation:15 || 6 = 156
. -
7290: 6 8 6 15
can be made true using6 * 8 || 6 * 15
. -
192: 17 8 14
can be made true using17 || 8 + 14
.
Adding up all six test values (the three that could be made before using only +
and *
plus the new three that can now be made by also using ||
) produces the new total calibration result of 11387
.
Using your new knowledge of elephant hiding spots, determine which equations could possibly be true. What is their total calibration result?
2.2.2 Solution
We simply adapt our is_valid1
function by adding the concat operator. We run the
backtracking algorithm only on cases where we did not find a match in part one, saving
also some decent amount of runs.
For the concat operator the (even more direly needed) branching condition is, whether the current result ends in the current number. If so, we recurse simply by removing those numbers from the end.
Without branching we would need a whooping number of
puzzle_data %>%
map_dbl(~ 3 ^ (.x$ops %>% length())) %>%
sum()
## [1] 57306960
runs, while in our implementation we brought that number down to 25,125.
is_valid2 <- function(res, operands, i, division = FALSE) {
nr_calls <<- nr_calls + 1L
ret <- FALSE
if (i == 0L) {
ret <- (division && res == 1L) || (!division && res == 0L)
} else {
operand <- operands[i]
if ((res - operand) >= 0L) {
if (Recall(res - operand, operands, i - 1L, FALSE)) {
ret <- TRUE
}
}
if (res %% operand == 0L) {
if (Recall(res / operand, operands, i - 1L, TRUE)) {
ret <- TRUE
}
}
str_operand <- as.character(operand)
str_res <- as.character(res)
n <- nchar(str_res)
n_operand <- nchar(str_operand)
if (substr(str_res, n - n_operand + 1L, n) == str_operand) {
## concatenate could be a viable solution
## remove last character and recurse
res <- substr(str_res, 1L, n - n_operand) %>%
as.integer64()
if (Recall(res, operands, i - 1L, FALSE)) {
ret <- TRUE
}
}
}
ret
}
idx2 <- get_valid(puzzle_data[!idx1], is_valid2)
res2 <- puzzle_data[!idx1][idx2] %>%
map("res") %>%
do.call(c, .) %>%
sum()
res1 + res2
## integer64
## [1] 145149066755184