Compare commits

..

10 commits

7 changed files with 766 additions and 0 deletions

180
day17/main.exs Normal file
View file

@ -0,0 +1,180 @@
defmodule Day17 do
def run do
[registers, program] =
File.read!("data.txt")
|> String.trim()
|> String.split("\n\n")
|> Enum.with_index()
|> Enum.map(fn {lines, idx} ->
if idx == 0, do: parseRegisters(lines), else: parseProgram(lines)
end)
# registers = %{A: 2024, B: 0, C: 0}
# program = [0, 1, 5, 4, 3, 0]
initial_state = %{
program: program,
registers: registers,
pointer: 0,
output: []
}
dbg(initial_state)
state = run(initial_state)
output = Enum.reverse(state.output)
dbg(output)
# part 2
# target is [2, 4, 1, 6, 7, 5, 4, 4, 1, 7, 0, 3, 5, 5, 3, 0]
# noticing every power of 8 it adds a digit
# the new big digit goes [1], 0, 3, 2, 4, 4, 1, 5
# when there's 1 digit, the biggest digit changes every 1
# when there are 2 digits, the biggest digit changes every 8
# when there are 3 digits, the biggest digit changes every 8 * 8
# etc.
# when there are 16 digits, the biggest digit changes every 8 ^ 15
# if the first digit is 2, that's the 3rd in the sequence, so 3x that
#
# 1 0 3 2 4 4 1 5
# when 2 digits, the smaller digit goes:
# 0: 1 0 3 2 6 5 1 1
# 3: 1 0 2 2 0 6 1 5
# 2: 1 0 2 2 2 7 1 1
# 4: 1 0 1 3 4 0 1 5
# 4: 1 0 1 3 6 1 1 1
# 1: 1 0 0 3 0 2 1 5
# 5: 1 0 0 3 2 3 1 1
#
# when 3 digits, smallest digit goes
# 0 0 7 0 4 4 1 5
#
# starter = 3 * 8 ** 15 + 6 * 8 ** 14 + 6 * 8 ** 13 # interesting, all ones
starter = 2 * 8 ** 15
increment = 8 ** 10
try(starter, initial_state, increment)
end
defp try(a, state, increment) do
# IO.gets("")
new_state = %{state | registers: %{state.registers | A: a}}
val = run(new_state)
# if Integer.mod(a, 1000) == 0 do
IO.puts(a)
dbg(a)
dbg(val)
# end
if Enum.reverse(val.output) == state.program do
a
else
try(a + increment, state, increment)
end
end
defp run(
%{
program: program,
registers: registers,
pointer: pointer,
output: output
} = state
) do
opcode = Enum.at(program, pointer)
operand = Enum.at(program, pointer + 1)
# dbg({"run", state})
if is_nil(opcode) do
state
else
new_state =
case opcode do
# adv
0 ->
num = registers[:A]
den = Integer.pow(2, getComboOperandValue(operand, registers))
new_regs = %{registers | A: trunc(num / den)}
%{state | registers: new_regs, pointer: pointer + 2}
# bxl
1 ->
new_regs = %{registers | B: Bitwise.bxor(registers[:B], operand)}
%{state | registers: new_regs, pointer: pointer + 2}
# bst
2 ->
new_regs = %{
registers
| B: Integer.mod(getComboOperandValue(operand, registers), 8)
}
%{state | registers: new_regs, pointer: pointer + 2}
# jnz
3 ->
if registers[:A] == 0 do
%{state | pointer: pointer + 2}
else
%{state | pointer: operand}
end
# bxc
4 ->
new_regs = %{registers | B: Bitwise.bxor(registers[:B], registers[:C])}
%{state | registers: new_regs, pointer: pointer + 2}
# out
5 ->
val = Integer.mod(getComboOperandValue(operand, registers), 8)
%{state | pointer: pointer + 2, output: [val | output]}
# bdv
6 ->
num = registers[:A]
den = Integer.pow(2, getComboOperandValue(operand, registers))
new_regs = %{registers | B: trunc(num / den)}
%{state | registers: new_regs, pointer: pointer + 2}
# cdv
7 ->
num = registers[:A]
den = Integer.pow(2, getComboOperandValue(operand, registers))
new_regs = %{registers | C: trunc(num / den)}
%{state | registers: new_regs, pointer: pointer + 2}
end
run(new_state)
end
end
defp parseProgram(line) do
[_header, digits] = String.split(line, " ")
digits |> String.split(",") |> Enum.map(&String.to_integer/1)
end
defp getComboOperandValue(op, registers) do
case op do
o when o <= 3 -> op
4 -> registers[:A]
5 -> registers[:B]
6 -> registers[:C]
end
end
defp parseRegisters(lines) do
lines
|> String.split("\n")
|> Enum.map(&String.split(&1, " "))
|> Enum.reduce(%{}, fn [_header, label, value], acc ->
atom = String.trim(label, ":") |> String.to_atom()
Map.put(acc, atom, String.to_integer(value))
end)
end
end
Day17.run()

112
day20/main.exs Normal file
View file

@ -0,0 +1,112 @@
defmodule Day20 do
def run do
{map, width, height, start, dest} = parse_input("data.txt")
# create a map of visited nodes an a queue
visited = %{start => 100_000_000}
q = :queue.from_list([{start, 0, []}])
{path, shortest} = walk(map, q, dest, visited, [], 100_000_000_000, width, height)
path = Enum.reverse(path)
part1 = run(2, path)
part2 = run(20, path)
dbg({part1, part2})
end
# walk every step along the path
# find coordinates that we can get to within <cheat> number of steps, that save us 100 picoseconds
defp run(cheats, path) do
Enum.with_index(path)
|> Enum.reduce(0, fn {el, idx}, acc ->
res =
path
|> Enum.with_index()
|> Enum.filter(fn {el2, idx2} ->
dist = distance(el, el2)
dist <= cheats and idx + dist + 100 <= idx2
end)
|> length
acc + res
end)
end
defp distance({x1, y1}, {x2, y2}) do
abs(x2 - x1) + abs(y2 - y1)
end
defp walk(map, q, {dx, dy} = dest, visited, paths, shortest_path, width, height) do
# dequeue the node with the lowest distnce
# no priority queue, so just find the lowest one and delete it from the queue
ql = :queue.to_list(q)
if length(ql) == 0 do
nil
else
# lowest = next_from_queue(ql, dest)
lowest = Enum.min_by(ql, fn {_, dist, _} -> dist end)
{{x, y}, dist, path} = lowest
new_q = :queue.delete(lowest, q)
# if we reached the destination
if x == dx and y == dy do
{[{x, y} | path], dist}
else
dirs = [{1, 0}, {0, 1}, {-1, 0}, {0, -1}]
# find neighbors to enqueue
neighbors =
dirs
|> Enum.map(fn {dx, dy} -> {x + dx, y + dy} end)
|> Enum.map(fn key -> {key, Map.get(map, key, ".")} end)
|> Enum.filter(fn {{x, y}, el} ->
el != "#" and x >= 0 and y >= 0 and x < width and y < height
end)
|> Enum.reject(fn {{nx, ny}, _el} -> Map.has_key?(visited, {nx, ny}) end)
|> Enum.reject(fn {{nx, ny}, _el} ->
Enum.any?(:queue.to_list(q), fn {pos, _, _} -> pos == {nx, ny} end)
end)
|> Enum.map(fn {key, _el} -> key end)
new_q2 =
Enum.reduce(neighbors, new_q, fn {nx, ny}, acc ->
:queue.in({{nx, ny}, dist + 1, [{x, y} | path]}, acc)
end)
new_visited = Map.put(visited, {x, y}, dist)
walk(map, new_q2, dest, new_visited, paths, shortest_path, width, height)
end
end
end
defp parse_input(file) do
map_list =
File.read!(file)
|> String.trim()
|> String.split("\n")
|> Enum.map(&String.graphemes/1)
height = length(map_list)
width = length(hd(map_list))
# convert the map List to a map Map, grabbing start and dest along the way
{map, start, dest} =
Enum.concat(map_list)
|> Enum.with_index()
|> Enum.reduce({%{}, nil, nil}, fn {el, idx}, {map, start, dest} ->
row = div(idx, height)
col = rem(idx, width)
map = Map.put(map, {row, col}, el)
start = if el == "S", do: {row, col}, else: start
dest = if el == "E", do: {row, col}, else: dest
{map, start, dest}
end)
{map, width, height, start, dest}
end
end
Day20.run()

181
day21/main.exs Normal file
View file

@ -0,0 +1,181 @@
defmodule Day21 do
def run do
lines =
File.read!("data.txt")
|> String.trim()
|> String.split("\n")
|> Enum.map(&String.graphemes/1)
numpad = %{
nil => {0, 0},
"0" => {1, 0},
"A" => {2, 0},
"1" => {0, 1},
"2" => {1, 1},
"3" => {2, 1},
"4" => {0, 2},
"5" => {1, 2},
"6" => {2, 2},
"7" => {0, 3},
"8" => {1, 3},
"9" => {2, 3}
}
dpad = %{
"<" => {0, 0},
"v" => {1, 0},
">" => {2, 0},
nil => {0, 1},
"^" => {1, 1},
"A" => {2, 1}
}
# doing some testing...
# go([[">", "v"], ["v", ">"]], dpad) |> go(dpad) |> dbg
# go([["<", "v"], ["v", "<"]], dpad) |> go(dpad) |> dbg
# go([[">", "^"], ["^", ">"]], dpad) |> go(dpad) |> dbg
# go([["<", "^"], ["^", "<"]], dpad) |> go(dpad) |> dbg
# reveals that:
# <, v is better than v, <
# v, > is better than >, v
# >, ^ is better than ^, >
# <, ^ is better than ^, <
part1 =
lines
|> go(numpad)
|> go(dpad)
|> go(dpad)
|> Enum.zip(lines)
|> Enum.map(fn {sequence, line} ->
numeric_part =
line |> Enum.drop(-1) |> Enum.map(&String.to_integer/1) |> Integer.undigits()
numeric_part * length(sequence)
end)
|> Enum.sum()
dbg(part1, charlists: :as_lists)
part2 =
lines
|> go(numpad)
|> Enum.zip(lines)
# for each line
|> Enum.reduce({%{}, 0}, fn {line, original_line}, {cache, sum} ->
# for each key
{new_cache, _, len} =
Enum.reduce(line, {cache, "A", 0}, fn key, {cache, previous, sum} ->
{num, cache} = num_steps_with_depth(previous, key, dpad, 25, cache)
{cache, key, sum + num}
end)
numeric_part =
original_line |> Enum.drop(-1) |> Enum.map(&String.to_integer/1) |> Integer.undigits()
{new_cache, sum + numeric_part * len}
end)
dbg(part2)
end
# finds the number of steps needed to get from a to b, over and over, depth times
defp num_steps_with_depth(a, b, keypad, depth, cache) do
key = {a, b, depth}
if Map.has_key?(cache, key) do
res = Map.get(cache, key)
{res, cache}
else
new_steps = steps(a, b, keypad)
case depth do
0 ->
new_cache = Map.put(cache, key, 2)
{2, new_cache}
1 ->
len = length(new_steps)
new_cache = Map.put(cache, key, len)
{len, new_cache}
d ->
# for each pair, find the length of depth - 1
{new_cache, el, steps} =
Enum.reduce(new_steps, {cache, "A", 0}, fn el, {cache, previous, num_steps} ->
{num, new_cache} = num_steps_with_depth(previous, el, keypad, depth - 1, cache)
new_cache = Map.put(new_cache, {previous, el, depth - 1}, num)
{new_cache, el, num + num_steps}
end)
{steps, new_cache}
end
end
end
# maps over each line, then reduces over each keypress, gathering the new steps needed
defp go(lines, pad) do
Enum.map(lines, fn line ->
Enum.reduce(line, {"A", []}, fn el, {previous, list} ->
{el, Enum.concat(list, steps(previous, el, pad))}
end)
|> elem(1)
end)
end
# returns the steps from a to b on the given keypad, including hitting A at the end
defp steps(a, b, keypad) do
{x1, y1} = Map.get(keypad, a)
{x2, y2} = Map.get(keypad, b)
y_char = if y2 > y1, do: "^", else: "v"
x_char = if x2 > x1, do: ">", else: "<"
xs =
if x2 == x1,
do: [],
else: Enum.reduce(1..abs(x2 - x1), [], fn _el, acc -> [x_char | acc] end)
ys =
if y2 == y1,
do: [],
else: Enum.reduce(1..abs(y2 - y1), [], fn _el, acc -> [y_char | acc] end)
{dx, dy} = Map.get(keypad, nil)
# avoid hole
res =
if x2 < x1 do
# if i'm going left, and going left _first_ hits the empty spot
if {x2, y1} == {dx, dy} do
# then go up/down first
Enum.concat([ys, xs, ["A"]])
end
else
# if i'm going up or down, and going up/down _first_ hits the empty spot
if y1 != y2 and {x1, y2} == {dx, dy} do
# then go left/right first
Enum.concat([xs, ys, ["A"]])
end
end
# if the hole needed to be avoided, return that
if !is_nil(res) do
res
else
# otherwise use our heuristic
cond do
# if we're going down and left, go left first
y2 < y1 and x2 < x1 -> Enum.concat([xs, ys, ["A"]])
# if we're going down and right, go down first
y2 < y1 and x2 > x1 -> Enum.concat([ys, xs, ["A"]])
# if we're going up and left, go left first
y2 > y1 and x2 < x1 -> Enum.concat([xs, ys, ["A"]])
# if we're going up and right (or anything else), go up first
true -> Enum.concat([ys, xs, ["A"]])
end
end
end
end
Day21.run()

73
day22/main.exs Normal file
View file

@ -0,0 +1,73 @@
defmodule Day22 do
def run do
lines =
File.read!("data.txt")
|> String.trim()
|> String.split("\n")
|> Enum.map(&String.to_integer/1)
part1 =
Enum.reduce(lines, 0, fn secret, acc ->
acc + Enum.reduce(1..2000, secret, fn _, s -> evolve(s) end)
end)
dbg(part1)
# build a map of all 4-digit sequences + list index to prices
map =
Enum.with_index(lines)
|> Enum.reduce(%{}, fn {secret, idx}, map ->
{_, prices, diffs, map2} =
Enum.reduce(1..2000, {secret, [price(secret)], [], map}, fn _, {s, prices, diffs, m} ->
new_secret = evolve(s)
price = price(new_secret)
diff = price - hd(prices)
new_diff = [diff | diffs]
sec = Enum.slice(new_diff, 0, 4)
new_map = Map.put_new(m, {sec, idx}, price)
{new_secret, [price | prices], new_diff, new_map}
end)
map2
end)
# for every possible sequence (there's only like 19 ^ 4 or something)
all = for a <- -9..9, b <- -9..9, c <- -9..9, d <- -9..9, do: [a, b, c, d]
part2 =
Enum.reduce(all, {0, []}, fn sec, {max, max_sec} ->
# and for every buyer, get the price at this sequence
val =
Enum.reduce(0..length(lines), 0, fn idx, acc ->
case Map.get(map, {sec, idx}, nil) do
nil -> acc
val -> val + acc
end
end)
if val > max, do: {val, sec}, else: {max, max_sec}
end)
dbg(part2)
end
defp evolve(secret) do
step1 = secret |> Kernel.*(64) |> mix(secret) |> prune()
step2 = step1 |> div(32) |> mix(step1) |> prune()
step2 |> Kernel.*(2048) |> mix(step2) |> prune()
end
defp mix(num, secret) do
Bitwise.bxor(num, secret)
end
defp prune(num) do
Integer.mod(num, 16_777_216)
end
defp price(num) do
Integer.digits(num) |> Enum.take(-1) |> Integer.undigits()
end
end
Day22.run()

81
day23/main.exs Normal file
View file

@ -0,0 +1,81 @@
defmodule Day23 do
def run do
connections =
File.read!("data.txt")
|> String.trim()
|> String.split("\n")
|> Enum.map(fn el -> String.split(el, "-") end)
all_computers = connections |> Enum.concat() |> Enum.uniq()
# build a map of computers -> all their connected computers
map =
Enum.reduce(all_computers, %{}, fn el1, map ->
friends =
Enum.filter(all_computers, fn el2 ->
Enum.member?(connections, [el1, el2]) || Enum.member?(connections, [el2, el1])
end)
Map.put(map, el1, friends)
end)
part1 =
expand(map, connections)
|> Enum.filter(fn set -> Enum.any?(set, fn el -> String.starts_with?(el, "t") end) end)
|> length
dbg(part1)
part2 =
loop_expand(map, connections)
|> hd()
|> Enum.join(",")
dbg(part2)
end
defp loop_expand(map, sets) do
case expand(map, sets) do
[] -> sets
x -> loop_expand(map, x)
end
end
defp expand(map, sets) do
# look at an element in every set (e.g. the first element)
# check all of its connections
# if any of those connected computers are connected to all of the set members, you've increased the set size by 1!
Enum.reduce(sets, [], fn set, acc ->
connections = Map.get(map, hd(set), [])
res =
Enum.reduce(connections, [], fn con, acc3 ->
# if it is connected to each member of the set, we've enlarged the set
if Enum.all?(set, fn set_el -> is_connected(map, set_el, con) end) do
[[con | set] | acc3]
else
acc3
end
end)
if Enum.empty?(res) do
acc
else
if is_list(Enum.at(res, 0)) do
Enum.concat(res, acc)
else
[res | acc]
end
end
end)
|> Enum.map(fn l -> Enum.sort(l) end)
|> Enum.uniq()
|> Enum.sort_by(fn a -> Enum.join(a) end)
end
defp is_connected(map, a, b) do
Map.get(map, a, []) |> Enum.member?(b)
end
end
Day23.run()

88
day24/main.exs Normal file
View file

@ -0,0 +1,88 @@
defmodule Day24 do
def run do
{wires, gates} = parse_input("data.txt")
# part1
part1 = run_loop(wires, gates) |> wire_value("z") |> b_to_d()
dbg(part1)
# part2
x = wire_value(wires, "x") |> b_to_d()
y = wire_value(wires, "y") |> b_to_d()
target = (x + y) |> d_to_b()
dbg(target)
dbg(b_to_d(target))
end
defp b_to_d(bin) do
:erlang.binary_to_integer(Integer.to_string(bin), 2)
end
defp d_to_b(dec) do
Integer.to_string(dec, 2) |> String.to_integer()
end
defp wire_value(wires, char) do
wires
|> Enum.filter(fn {k, _} -> String.starts_with?(k, char) end)
|> Enum.sort()
|> Enum.reduce([], fn {_, v}, acc -> [v | acc] end)
|> Integer.undigits()
end
defp run_loop(wires, gates) do
new_wires = run(wires, gates)
if map_size(new_wires) == map_size(wires) do
wires
else
run_loop(new_wires, gates)
end
end
defp run(wires, gates) do
Enum.reduce(gates, wires, fn %{left: left, right: right, output: output, op: op}, acc ->
l = Map.get(wires, left, nil)
r = Map.get(wires, right, nil)
if !is_nil(l) and !is_nil(r) do
Map.put(acc, output, op.(l, r))
else
acc
end
end)
end
defp parse_input(file) do
[wires, gates] =
File.read!(file)
|> String.trim()
|> String.split("\n\n")
|> Enum.map(&String.split(&1, "\n"))
wires =
Enum.reduce(wires, %{}, fn el, map ->
[wire, value] = String.split(el, ": ")
Map.put(map, wire, String.to_integer(value))
end)
gates =
Enum.reduce(gates, [], fn el, map ->
[logic, output] = String.split(el, " -> ")
[left, op, right] = String.split(logic, " ")
op =
case op do
"AND" -> &Bitwise.band/2
"OR" -> &Bitwise.bor/2
"XOR" -> &Bitwise.bxor/2
end
[%{left: left, right: right, op: op, output: output} | map]
end)
{wires, gates}
end
end
Day24.run()

51
day25/main.exs Normal file
View file

@ -0,0 +1,51 @@
defmodule Day25 do
def run do
%{locks: locks, keys: keys} = parse_input("data.txt")
# compare every key against every lock
good_keys =
Enum.reduce(keys, 0, fn key, acc ->
acc +
Enum.reduce(locks, 0, fn lock, acc2 ->
acc2 + if fits(lock, key), do: 1, else: 0
end)
end)
dbg(good_keys)
end
defp fits(lock, key) do
Enum.reduce(0..4, true, fn idx, acc ->
acc and Enum.at(lock, idx) + Enum.at(key, idx) <= 5
end)
end
defp parse_input(file) do
File.read!(file)
|> String.trim()
|> String.split("\n\n")
|> Enum.reduce(%{locks: [], keys: []}, fn el, %{locks: locks, keys: keys} = acc ->
grid = String.split(el, "\n") |> Enum.map(&String.graphemes/1)
is_lock = grid |> hd() |> hd() |> String.graphemes() |> Enum.all?(fn el -> el == "#" end)
res =
Enum.map(0..4, fn col ->
Enum.reduce(0..6, 0, fn row, acc2 ->
el = Enum.at(grid, row) |> Enum.at(col)
case el do
"#" -> acc2 + 1
_ -> acc2
end
end) - 1
end)
case is_lock do
true -> %{acc | locks: [res | locks]}
false -> %{acc | keys: [res | keys]}
end
end)
end
end
Day25.run()