diff --git a/day17/main.exs b/day17/main.exs new file mode 100644 index 0000000..07c9bc4 --- /dev/null +++ b/day17/main.exs @@ -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() diff --git a/day20/main.exs b/day20/main.exs new file mode 100644 index 0000000..3d99c3d --- /dev/null +++ b/day20/main.exs @@ -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 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() diff --git a/day21/main.exs b/day21/main.exs new file mode 100644 index 0000000..5044ae6 --- /dev/null +++ b/day21/main.exs @@ -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() diff --git a/day22/main.exs b/day22/main.exs new file mode 100644 index 0000000..bfdee2d --- /dev/null +++ b/day22/main.exs @@ -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() diff --git a/day23/main.exs b/day23/main.exs new file mode 100644 index 0000000..fa6a371 --- /dev/null +++ b/day23/main.exs @@ -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() diff --git a/day24/main.exs b/day24/main.exs new file mode 100644 index 0000000..7a4b6f1 --- /dev/null +++ b/day24/main.exs @@ -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() diff --git a/day25/main.exs b/day25/main.exs new file mode 100644 index 0000000..e56f4b1 --- /dev/null +++ b/day25/main.exs @@ -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()