# Libraries
library(tidyverse)
# Read input from file
<- read_lines("../input/day24.txt", skip_empty_rows = TRUE) input
Day 24
Advent of Code: Worked Solutions
Setup
Part 1
Convert input to lists of gates and wires:
<- input |>
init ::unglue_data("{wire}: {value}", convert = TRUE) |>
ungluefilter(!is.na(wire))
<- input |>
gates ::unglue_data("{src1} {gate} {src2} -> {target}") |>
ungluefilter(!is.na(gate)) |>
mutate(gate = case_match(gate, "AND" ~ "&", "OR" ~ "|", "XOR" ~ "xor"))
<- init |>
wires complete(wire = unique(c(gates$src1, gates$src2, gates$target))) |>
deframe()
Loop through the lists of gates and wires, and whenever a new gate can be activated, get its resulting value. Repeat until all wires have a final output.
<- wires |>
df enframe(name = "wire") |>
left_join(gates, join_by(wire == target))
repeat {
<- df |>
cur_values select(wire, value) |>
deframe()
<- df |>
df mutate(
val1 = cur_values[src1],
val2 = cur_values[src2],
value = coalesce(
value,pmap_int(list(gate, val1, val2), \(gate, val1, val2) {
if (!is.na(gate) & !is.na(val1) & !is.na(val2))
get(gate)(val1, val2)
else
NA_integer_
})
)
)
if (all(!is.na(df$value))) break
}
Convert the Z-coded wires to a binary number:
|>
df filter(str_starts(wire, "z")) |>
arrange(wire) |>
pull(value) |>
imap_dbl(\(x, i) x * 2^(i - 1)) |>
sum() |>
format(scientific = FALSE)
[1] "59364044286798"
Part 2
Thanks to hint from Reddit: this method of binary addition using only AND, OR, XOR gates without negation is a “ripple-carry adder.”
The gates follow a consistent algorithm, where “Z” digits are the final output digits, and “C” values are carried over to the next digit:
STEP 00:
Z00 = X00 XOR Y00 (Final output: Z00)
C01 = X00 AND Y00 (Carry forward to next step)
STEP 01:
Z01A = X01 XOR Y01 (Intermediate step)
Z01 = Z01A XOR C01 (Final output: Z01)
C02A = X01 AND Y01 (Intermediate step)
C02B = Z01A AND C01 (Intermediate step)
C02 = C02A OR C02B (Carry forward to next step)
(...)
C44 = C44A OR C44B (Carry forward to next step)
STEP 44:
Z44A = X44 XOR Y44 (Intermediate step)
Z44 = Z44A XOR C44 (Final output: Z45)
C45A = X44 AND Y44 (Intermediate step)
C45B = Z44A AND C44 (Intermediate step)
C45 = C45A OR C45B (No further steps to carry over. Set as final output: Z45)
We can compare the expected versus actual logic to find steps that don’t match this algorithm.
# Pull lists of all wires of x, y, z, and other types for quick reference
<- discard(names(wires), ~ str_starts(.x, "x|y|z"))
nlist <- keep(names(wires), ~ str_starts(.x, "x"))
xlist <- keep(names(wires), ~ str_starts(.x, "y"))
ylist <- keep(names(wires), ~ str_starts(.x, "z"))
zlist <- c(xlist, ylist)
xylist <- max(zlist)
zmax
# Put source gates in alphabetical order for easier comparison
<- gates |>
gates mutate(src = map2(src1, src2, ~ sort(c(.x, .y)))) |>
select(-c(src1, src2)) |>
unnest_wider(src, names_sep = "")
Pull invalid outputs according to their gate type and inputs:
<- gates |>
invalid_by_gate pmap_lgl(\(gate, target, src1, src2) {
case_when(
# All z-target cases:
== "z00" ~ src1 == "x00" & src2 == "y00" & gate == "xor",
target == zmax ~ src1 %in% nlist & src2 %in% nlist & gate == "|",
target %in% zlist ~ src1 %in% nlist & src2 %in% nlist & gate == "xor",
target # N-target cases by gate type:
== "xor" ~ src1 %in% xylist & src2 %in% xylist,
gate == "|" ~ src1 %in% nlist & src2 %in% nlist
gate
)|>
}) set_names(gates$target) |>
keep(~ !is.na(.x) & .x == FALSE) |>
names()
Identify invalid outputs by following their logic trail forward and checking the gate types of the logic they are used as later input for:
<- gates |>
invalid_by_path left_join(select(gates, src1, gate), join_by(x$target == y$src1), suffix = c("", "1")) |>
left_join(select(gates, src2, gate), join_by(x$target == y$src2), suffix = c("", "2")) |>
nest(nxt = c(gate1, gate2), .by = -c(gate1, gate2)) |>
mutate(
nxt = map(nxt, ~ .x |> unlist() |> discard(is.na) |> unname())
|>
) unnest_wider(nxt, names_sep = "_") |>
mutate(
valid = case_when(
%in% zlist
target ~ NA,
== "xor"
gate ~ nxt_1 %in% c("xor", "&") &
%in% c("xor", "&") &
nxt_2 != nxt_2 &
nxt_1 !is.na(nxt_1) &
!is.na(nxt_2),
== "|"
gate ~ nxt_1 %in% c("xor", "&") &
%in% c("xor", "&") &
nxt_2 != nxt_2 &
nxt_1 !is.na(nxt_1) &
!is.na(nxt_2),
== "&" & !(src1 %in% c("x00", "y00"))
gate ~ nxt_1 == "|" | nxt_2 == "|"
)|>
) filter(!valid) |>
pull(target)
Concatenate all invalid gates alphabetically:
c(invalid_by_gate, invalid_by_path) |>
unique() |>
sort() |>
str_c(collapse = ",")
[1] "cbj,cfk,dmn,gmt,qjj,z07,z18,z35"