diff --git a/.gitignore b/.gitignore index b263cd1..be9fcb6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ erl_crash.dump *.beam /config/*.secret.exs .elixir_ls/ +day*.txt +input/ + diff --git a/2024/day1.exs b/2024/day1.exs new file mode 100644 index 0000000..f02172f --- /dev/null +++ b/2024/day1.exs @@ -0,0 +1,42 @@ +getLeftAndRightLists = fn -> + stream = + File.stream!("test/day1.txt") + |> Stream.map(&String.trim/1) + |> Stream.map(fn line -> + [l, r] = String.split(line) + {String.to_integer(l), String.to_integer(r)} + end) + |> Enum.to_list() + + Enum.unzip(stream) +end + +part1 = fn -> + {ls, rs} = getLeftAndRightLists.() + sortedLs = Enum.sort(ls) + sortedRs = Enum.sort(rs) + + result = + Enum.zip(sortedLs, sortedRs) + |> Enum.map(fn {l, r} -> abs(l - r) end) + |> Enum.sum() + + result +end + +part2 = fn -> + {ls, rs} = getLeftAndRightLists.() + + Enum.reduce( + ls, + 0, + fn l, acc -> + c = l * Enum.count(rs, fn r -> r == l end) + acc + c + end + ) +end + +IO.puts("part 1 - " <> Integer.to_string(part1.())) + +IO.puts("part 2 - " <> Integer.to_string(part2.())) diff --git a/2024/day10.exs b/2024/day10.exs new file mode 100644 index 0000000..a2dae1b --- /dev/null +++ b/2024/day10.exs @@ -0,0 +1,69 @@ +Code.require_file("./grid.ex") + +defmodule Day10 do + defp file(), do: "test/day10.txt" + + def part1() do + grid = Grid.parse(file(), &String.to_integer/1) + + for start <- find_start_points(grid) do + find_trails(grid, start, [], MapSet.new()) + |> List.flatten() + |> Enum.uniq() + |> Enum.count() + end + |> Enum.sum() + end + + def part2() do + grid = Grid.parse(file(), &String.to_integer/1) + + for start <- find_start_points(grid) do + find_trails(grid, start, [], MapSet.new()) + |> List.flatten() + |> Enum.count() + end + |> Enum.sum() + end + + defp find_trails(grid, current, acc, visited) do + current_tile = Grid.at(grid, current) + + if current_tile == 9 do + [current | acc] + else + new_visited = MapSet.put(visited, current) + + for move <- find_moves(grid, current, new_visited) do + find_trails(grid, move, acc, new_visited) |> List.flatten() + end ++ acc + end + end + + defp find_moves(grid, current, visited) do + for move <- [Grid.up(), Grid.down(), Grid.left(), Grid.right()] do + new_pos = Grid.add(current, move) + + if !MapSet.member?(visited, new_pos) && Grid.contains?(grid, new_pos) && + Grid.at(grid, new_pos) == Grid.at(grid, current) + 1 do + new_pos + end + end + |> Enum.filter(&Function.identity/1) + end + + defp find_start_points(grid) do + for {row, y} <- Tuple.to_list(grid) |> Enum.with_index(), + {elem, x} <- Tuple.to_list(row) |> Enum.with_index() do + if elem == 0 do + {x, y} + end + end + |> Enum.filter(&Function.identity/1) + end +end + +IO.puts("part 1") +Day10.part1() |> IO.inspect() +IO.puts("part 2") +Day10.part2() |> IO.inspect() diff --git a/2024/day11.exs b/2024/day11.exs new file mode 100644 index 0000000..519271f --- /dev/null +++ b/2024/day11.exs @@ -0,0 +1,54 @@ +defmodule Day11 do + defp parse() do + File.read!("test/day11.txt") + |> String.trim() + |> String.split() + |> Enum.map(&String.to_integer/1) + end + + def solve(blinks) do + terms = parse() + + start = + for term <- terms, into: %{} do + {term, 1} + end + + stone_counts = + for _i <- 0..(blinks - 1), reduce: start do + acc -> + blink(acc) + end + + Map.values(stone_counts) |> Enum.sum() + end + + defp blink(stones) do + for {stone, count} <- Map.to_list(stones), reduce: %{} do + acc -> + cond do + stone == 0 -> + Map.update(acc, 1, count, &(&1 + count)) + + rem(count_digits(stone), 2) == 0 -> + digits = Integer.digits(stone) + + {first_digits, second_digits} = + Enum.split(digits, Integer.floor_div(Enum.count(digits), 2)) + + Map.update(acc, Integer.undigits(first_digits), count, &(&1 + count)) + |> Map.update(Integer.undigits(second_digits), count, &(&1 + count)) + + true -> + Map.update(acc, stone * 2024, count, &(&1 + count)) + end + end + end + + defp count_digits(i), do: Integer.digits(i) |> Enum.count() +end + +IO.puts("part 1") +Day11.solve(25) |> IO.inspect() +IO.puts("part 2") +Day11.solve(75) |> IO.inspect() diff --git a/2024/day12.exs b/2024/day12.exs new file mode 100644 index 0000000..b982830 --- /dev/null +++ b/2024/day12.exs @@ -0,0 +1,110 @@ +Code.require_file("./grid.ex") + +defmodule Day12 do + defp file(), do: "test/day12.txt" + + def part1() do + grid = Grid.parse(file()) + + regions(Grid.as_map(grid), []) + |> Enum.map(fn region -> + region_set = MapSet.new(region) + MapSet.size(region_set) * perimiter(region) + end) + |> Enum.sum() + end + + def part2() do + grid = Grid.parse(file()) + + regions(Grid.as_map(grid), []) + |> Enum.map(fn region -> + region_set = MapSet.new(region) + MapSet.size(region_set) * sides(region) + end) + |> Enum.sum() + end + + defp regions(tiles, acc) when map_size(tiles) == 0, do: acc + + defp regions(tiles, acc) do + {pos, plant} = Enum.at(tiles, 0) + {rest_tiles, region} = fill_region(tiles, plant, pos, []) + regions(rest_tiles, [region | acc]) + end + + defp fill_region(tiles, plant, pos, acc) do + if tiles[pos] == plant do + acc = [pos | acc] + tiles = Map.delete(tiles, pos) + + for neighb <- Grid.neighbours(pos), reduce: {tiles, acc} do + {tiles, acc} -> fill_region(tiles, plant, neighb, acc) + end + else + {tiles, acc} + end + end + + defp perimiter(region) do + border_tiles(region) + |> Enum.map(fn tile -> + Grid.neighbours(tile) + |> Enum.count(&(&1 not in region)) + end) + |> Enum.sum() + end + + def border_tiles(region) do + Enum.filter(region, fn tile -> + Grid.neighbours(tile) + |> Enum.any?(&(&1 not in region)) + end) + end + + def sides(region) do + borders = + border_tiles(region) + |> Enum.flat_map(fn tile -> + acc = (Grid.up(tile) in region && []) || [{tile, :up}] + acc = (Grid.down(tile) in region && acc) || [{tile, :down} | acc] + acc = (Grid.left(tile) in region && acc) || [{tile, :left} | acc] + (Grid.right(tile) in region && acc) || [{tile, :right} | acc] + end) + |> MapSet.new() + + count_sides(borders, 0) + end + + defp remove_border(border_tiles, {tile, dir} = entry) when dir in [:up, :down] do + if entry in border_tiles do + border_tiles = MapSet.delete(border_tiles, entry) + remove_border(border_tiles, {Grid.right(tile), dir}) + else + border_tiles + end + end + + defp remove_border(border_tiles, {tile, dir} = entry) when dir in [:left, :right] do + if entry in border_tiles do + border_tiles = MapSet.delete(border_tiles, entry) + remove_border(border_tiles, {Grid.down(tile), dir}) + else + border_tiles + end + end + + defp count_sides(borders, acc) do + if MapSet.size(borders) == 0 do + acc + else + borders = remove_border(borders, Enum.min(borders)) + count_sides(borders, acc + 1) + end + end +end + +IO.puts("part 1") +Day12.part1() |> IO.inspect() +IO.puts("part 2") +Day12.part2() |> IO.inspect() diff --git a/2024/day13.exs b/2024/day13.exs new file mode 100644 index 0000000..e1a813e --- /dev/null +++ b/2024/day13.exs @@ -0,0 +1,50 @@ +defmodule Day13 do + defp lines() do + File.stream!("test/day13.txt") + |> Enum.map(&String.trim/1) + |> Enum.reject(&(&1 == "")) + end + + defp parse([], _, acc), do: acc + + defp parse(lines, prize_ext, acc) do + {[a_line, b_line, prize_line], rest} = Enum.split(lines, 3) + + {px, py} = get_x_y(prize_line) + crane = %{a: get_x_y(a_line), b: get_x_y(b_line), prize: {px + prize_ext, py + prize_ext}} + parse(rest, prize_ext, [crane | acc]) + end + + defp get_x_y(line) do + [_, x, y] = Regex.run(~r/X.([0-9]+), Y.([0-9]+)/, line) + {String.to_integer(x), String.to_integer(y)} + end + + def solve(prize_ext) do + cranes = parse(lines(), prize_ext, []) + + for crane <- cranes, reduce: 0 do + acc -> + acc + cost(crane) + end + end + + defp cost(%{:a => {ax, ay}, :b => {bx, by}, :prize => {px, py}}) do + + n = (px * by - py * bx) / (ax * by - bx * ay) + m = (px - ax * n) / bx + + if whole(n) && whole(m) do + trunc(n * 3 + m) + else + 0 + end + end + + defp whole(n), do: trunc(n) == n +end + +IO.puts("part 1") +Day13.solve(0) |> IO.inspect() +IO.puts("part 2") +Day13.solve(10000000000000) |> IO.inspect() diff --git a/2024/day14.exs b/2024/day14.exs new file mode 100644 index 0000000..5e29f79 --- /dev/null +++ b/2024/day14.exs @@ -0,0 +1,104 @@ +defmodule Day14 do + defp parse(file) do + File.stream!(file) + |> Enum.map(&String.trim/1) + |> Enum.map(fn line -> + [_, x, y, dx, dy] = Regex.run(~r/p=([0-9]+),([0-9]+) v=(-?[0-9]+),(-?[0-9]+)/, line) + {{int(x), int(y)}, {int(dx), int(dy)}} + end) + end + + defp int(s), do: String.to_integer(s) + + def part1(file, w, h) do + final_positions = + for robot <- parse(file) do + move(robot, 100, w, h) + end + + half_h = Integer.floor_div(h - 1, 2) + half_w = Integer.floor_div(w - 1, 2) + + %{:q1 => q1, :q2 => q2, :q3 => q3, :q4 => q4} = + for {x, y} <- final_positions, reduce: %{} do + acc -> + cond do + x < half_w && y < half_h -> Map.update(acc, :q1, 1, &(&1 + 1)) + x > half_w && y < half_h -> Map.update(acc, :q2, 1, &(&1 + 1)) + x < half_w && y > half_h -> Map.update(acc, :q3, 1, &(&1 + 1)) + x > half_w && y > half_h -> Map.update(acc, :q4, 1, &(&1 + 1)) + true -> acc + end + end + + q1 * q2 * q3 * q4 + end + + defp move({{start_x, start_y}, {dx, dy}}, seconds, w, h) do + x = start_x + dx * seconds + y = start_y + dy * seconds + + {wrap(x, w), wrap(y, h)} + end + + defp wrap(coord, max) do + c = + if(coord < 0) do + norm = Integer.floor_div(-coord, max) + 1 + coord + max * norm + else + coord + end + + rem(c, max) + end + + def part2(file, w, h) do + robots = parse(file) + find_line(robots, 1, 100_000, w, h) + end + + defp find_line(robots, seconds, max, w, h) do + if seconds > max do + nil + else + by_y = + for robot <- robots, reduce: %{} do + acc -> + {x, y} = move(robot, seconds, w, h) + Map.update(acc, y, [x], &[x | &1]) + end + + found_line? = Map.values(by_y) + |> Enum.any?(fn xs -> find_longest_line(xs) >= 10 end) + + if(found_line?) do + seconds + else + find_line(robots, seconds + 1, max, w, h) + end + end + end + + def find_longest_line(xs) do + Enum.sort(xs) + |> Enum.scan({0, -1}, fn x, {count, prevx} -> + if x == prevx + 1 do + {count + 1, x} + else + {1, x} + end + end) + |> Enum.map(&elem(&1, 0)) + |> Enum.max() + end +end + +file = "test/day14.txt" +width = 101 +height = 103 + +IO.puts("part 1") +Day14.part1(file, width, height) |> IO.inspect() +IO.puts("part 2") +Day14.part2(file, width, height) |> IO.inspect() diff --git a/2024/day15.exs b/2024/day15.exs new file mode 100644 index 0000000..28b964e --- /dev/null +++ b/2024/day15.exs @@ -0,0 +1,238 @@ +Code.require_file("./grid.ex") + +defmodule Day15 do + defp parse(file, room_translate) do + {room, moves} = + File.stream!(file) + |> Enum.map(&String.trim/1) + |> Enum.split_while(&(&1 != "")) + + moves = + Enum.drop(moves, 1) |> Enum.join() |> String.graphemes() |> Enum.map(&String.to_atom/1) + + {Grid.parse_lines(room_translate.(room), &String.to_atom/1), moves} + end + + def part1(file) do + {room, moves} = parse(file, &Function.identity/1) + start_at = Grid.find(room, &(&1 == :@)) + + room = do_moves(moves, start_at, room) + + for {row, y} <- Tuple.to_list(room) |> Enum.with_index(), + {el, x} <- Tuple.to_list(row) |> Enum.with_index(), + reduce: 0 do + acc -> + if el == :O do + acc + 100 * y + x + else + acc + end + end + end + + defp do_moves([], _, room), do: room + + defp do_moves([move | moves], pos, room) do + delta = to_delta(move) + next_pos = Grid.add(pos, delta) + tile = Grid.at(room, next_pos) + + {pos_after_move, room_after_move} = + case(tile) do + :. -> + {next_pos, Grid.set(room, pos, :.) |> Grid.set(next_pos, :@)} + + :"#" -> + {pos, room} + + :O -> + move_box(pos, delta, room) + + :"[" -> + move_big_box(pos, next_pos, next_pos, Grid.right(next_pos), delta, room) + + :"]" -> + move_big_box(pos, next_pos, Grid.left(next_pos), next_pos, delta, room) + end + + do_moves(moves, pos_after_move, room_after_move) + end + + defp move_box(pos, delta, room) do + next_pos = Grid.add(pos, delta) + + ahead = ahead(next_pos, delta, room) + + {_, non_boxes} = Enum.split_while(ahead, &(Grid.at(room, &1) == :O)) + non_box = hd(non_boxes) + non_box_tile = Grid.at(room, non_box) + + case(non_box_tile) do + :"#" -> + {pos, room} + + :. -> + room = + Grid.set(room, pos, :.) + |> Grid.set(next_pos, :@) + |> Grid.set(non_box, :O) + + {next_pos, room} + end + end + + defp to_delta(move) do + case(move) do + :> -> Grid.right() + :^ -> Grid.up() + :< -> Grid.left() + :v -> Grid.down() + end + end + + def part2(file) do + {room, moves} = parse(file, &widen/1) + start_at = Grid.find(room, &(&1 == :@)) + + room = do_moves(moves, start_at, room) + + for {row, y} <- Tuple.to_list(room) |> Enum.with_index(), + {el, x} <- Tuple.to_list(row) |> Enum.with_index(), + reduce: 0 do + acc -> + if el == :"[" do + acc + 100 * y + x + else + acc + end + end + end + + defp widen(lines) do + for line <- lines do + for token <- String.graphemes(line), reduce: "" do + acc -> + case token do + "#" -> acc <> "##" + "O" -> acc <> "[]" + "." -> acc <> ".." + "@" -> acc <> "@." + end + end + end + end + + defp move_big_box(robot_current, robot_to, box_l, box_r, delta, room) do + {room, changed} = do_box_move(box_l, box_r, delta, room) + + if(changed) do + room = Grid.set(room, robot_current, :.) |> Grid.set(robot_to, :@) + {robot_to, room} + else + {robot_current, room} + end + end + + defp do_box_move(box_l, box_r, delta, room) do + next_l_pos = Grid.add(box_l, delta) + next_r_pos = Grid.add(box_r, delta) + next_l = Grid.at(room, next_l_pos) + next_r = Grid.at(room, next_r_pos) + + cond do + next_l == :"#" || next_r == :"#" -> + {room, false} + + delta == Grid.left() && next_l == :. -> + {Grid.set(room, next_l_pos, :"[") |> Grid.set(next_r_pos, :"]") |> Grid.set(box_r, :.), + true} + + delta == Grid.right() && next_r == :. -> + {Grid.set(room, next_l_pos, :"[") |> Grid.set(next_r_pos, :"]") |> Grid.set(box_l, :.), + true} + + next_l == :. && next_r == :. -> + { + Grid.set(room, next_l_pos, :"[") + |> Grid.set(next_r_pos, :"]") + |> Grid.set(box_l, :.) + |> Grid.set(box_r, :.), + true + } + + delta == Grid.right() && next_r == :"[" -> + {room, moved} = do_box_move(next_r_pos, Grid.right(next_r_pos), delta, room) + + if moved do + # Move again now we've made space + do_box_move(box_l, box_r, delta, room) + else + {room, false} + end + + delta == Grid.left() && next_l == :"]" -> + {room, moved} = do_box_move(Grid.left(next_l_pos), next_l_pos, delta, room) + + if moved do + # Move again now we've made space + do_box_move(box_l, box_r, delta, room) + else + {room, false} + end + + next_l == :"[" && next_r == :"]" -> + {room, moved} = do_box_move(next_l_pos, next_r_pos, delta, room) + + if moved do + # Move again now we've made space + do_box_move(box_l, box_r, delta, room) + else + {room, false} + end + + next_l == :"]" && next_r == :"[" -> + # two boxes above + {l_room, l_moved} = do_box_move(Grid.left(next_l_pos), next_l_pos, delta, room) + {_, r_moved} = do_box_move(next_r_pos, Grid.right(next_r_pos), delta, room) + + if l_moved && r_moved do + {room, _} = do_box_move(next_r_pos, Grid.right(next_r_pos), delta, l_room) + do_box_move(box_l, box_r, delta, room) + else + {room, false} + end + + (Grid.up() == delta || Grid.down() == delta) && next_l == :"]" -> + {room, moved} = do_box_move(Grid.left(next_l_pos), next_l_pos, delta, room) + + if moved do + # Move again now we've made space + do_box_move(box_l, box_r, delta, room) + else + {room, false} + end + + (Grid.up() == delta || Grid.down() == delta) && next_r == :"[" -> + {room, moved} = do_box_move(next_r_pos, Grid.right(next_r_pos), delta, room) + + if moved do + # Move again now we've made space + do_box_move(box_l, box_r, delta, room) + else + {room, false} + end + end + end + + defp ahead(pos, delta, room) do + Stream.iterate(pos, &Grid.add(&1, delta)) + |> Enum.take_while(&Grid.contains?(room, &1)) + end +end + +file = "test/day15.txt" +IO.puts("part 1") +Day15.part1(file) |> IO.inspect() +IO.puts("part 2") +Day15.part2(file) |> IO.inspect() diff --git a/2024/day16.exs b/2024/day16.exs new file mode 100644 index 0000000..55af49d --- /dev/null +++ b/2024/day16.exs @@ -0,0 +1,153 @@ +Code.require_file("./grid.ex") + +defmodule Day16 do + defmodule Move do + defstruct [:pos, :dir, :score, :path] + end + + def part1(file) do + maze = Grid.parse(file, &String.to_atom/1) + Grid.print(maze) + start = Grid.find(maze, &(&1 == :S)) + maze = Grid.set(maze, start, :.) + search(maze, MapSet.new(), [%Move{pos: start, dir: Grid.right(), score: 0}]) + end + + defp search(_, _, []), do: raise("lost") + + defp search(maze, visited, [current | move_queue]) do + tile = Grid.at(maze, current.pos) + new_visited = MapSet.put(visited, {current.pos, current.dir}) + + case(tile) do + :E -> + current.score + + # This move was bad, just drop it + :"#" -> + search(maze, new_visited, move_queue) + + :. -> + queue = [ + %Move{ + pos: Grid.add(current.pos, current.dir), + dir: current.dir, + score: current.score + 1 + }, + %Move{ + pos: current.pos, + dir: turn_clock(current.dir), + score: current.score + 1000 + }, + %Move{ + pos: current.pos, + dir: turn_anti(current.dir), + score: current.score + 1000 + } + | move_queue + ] + + queue = + Enum.reject(queue, &({&1.pos, &1.dir} in visited)) + |> Enum.sort_by(& &1.score) + + search(maze, new_visited, queue) + end + end + + def part2(file, target) do + maze = Grid.parse(file, &String.to_atom/1) + start = Grid.find(maze, &(&1 == :S)) + maze = Grid.set(maze, start, :.) + + queue = :queue.new() + queue = :queue.in(%Move{pos: start, dir: Grid.right(), score: 0, path: [start]}, queue) + + search_all( + maze, + %{start: Grid.right()}, + target, + queue, + [] + ) + |> List.flatten() + |> Enum.uniq() + |> Enum.count() + end + + defp search_all(maze, visited, target, queue, acc) do + if :queue.is_empty(queue) do + acc + else + do_search_all(maze, visited, target, queue, acc) + end + end + + defp do_search_all(maze, visited, target, queue, acc) do + {{_, current}, queue} = :queue.out(queue) + tile = Grid.at(maze, current.pos) + new_visited = Map.put(visited, {current.pos, current.dir}, current.score) + + cond do + current.score > Map.get(visited, {current.pos, current.dir}, 100_000_000) -> + search_all(maze, visited, target, queue, acc) + + tile == :"#" -> + search_all(maze, visited, target, queue, acc) + + current.score > target -> + search_all(maze, visited, target, queue, acc) + + tile == :E -> + search_all(maze, visited, target, queue, [current.path | acc]) + + tile == :. -> + new_pos = Grid.add(current.pos, current.dir) + + queue = + :queue.in( + %Move{ + pos: new_pos, + dir: current.dir, + score: current.score + 1, + path: [new_pos | current.path] + }, + queue + ) + + queue = + :queue.in( + %Move{ + pos: current.pos, + dir: turn_clock(current.dir), + score: current.score + 1000, + path: current.path + }, + queue + ) + + queue = + :queue.in( + %Move{ + pos: current.pos, + dir: turn_anti(current.dir), + score: current.score + 1000, + path: current.path + }, + queue + ) + + search_all(maze, new_visited, target, queue, acc) + end + end + + defp turn_clock({x, y}), do: {-y, x} + + defp turn_anti({x, y}), do: {y, -x} +end + +file = "test/day16.txt" +IO.puts("part 1") +target = Day16.part1(file) |> IO.inspect() +IO.puts("part 2") +Day16.part2(file, target) |> IO.inspect() diff --git a/2024/day17.exs b/2024/day17.exs new file mode 100644 index 0000000..3f71ba2 --- /dev/null +++ b/2024/day17.exs @@ -0,0 +1,138 @@ +defmodule Day17 do + defmodule Computer do + defstruct [:a, :b, :c, ip: 0, output: []] + end + + defp parse(file) do + lines = File.stream!(file) |> Enum.map(&String.trim/1) |> Enum.to_list() + + [a, b, c, _, p] = lines + + computer = %Computer{ + a: int(value(a)), + b: int(value(b)), + c: int(value(c)) + } + + codes = String.split(value(p), ",") |> Enum.map(&int/1) + {computer, codes} + end + + defp int(s), do: String.to_integer(s) + + defp value(line) do + String.split(line, ":") |> Enum.at(1) |> String.trim() + end + + def part1(file) do + {computer, codes} = parse(file) + computer = evaluate(computer, codes) + Enum.reverse(computer.output) |> Enum.join(",") + end + + # Halt + defp evaluate(computer, codes) when computer.ip >= length(codes), do: computer + + defp evaluate(computer, codes) do + [op, operand] = Enum.slice(codes, computer.ip..(computer.ip + 1)) + + computer = + case(op) do + 0 -> adv(computer, operand) |> advance_ip() + 1 -> bxl(computer, operand) |> advance_ip() + 2 -> bst(computer, operand) |> advance_ip() + 3 -> jnz(computer, operand) + 4 -> bxc(computer) |> advance_ip() + 5 -> out(computer, operand) |> advance_ip() + 6 -> bdv(computer, operand) |> advance_ip() + 7 -> cdv(computer, operand) |> advance_ip() + end + + evaluate(computer, codes) + end + + defp eval_combo(computer, combo) do + case combo do + 4 -> computer.a + 5 -> computer.b + 6 -> computer.c + 7 -> raise("777") + _ -> combo + end + end + + defp adv(computer, operand) do + d = Integer.pow(2, eval_combo(computer, operand)) + %{computer | a: Integer.floor_div(computer.a, d)} + end + + defp bxl(computer, operand) do + result = Bitwise.bxor(computer.b, operand) + %{computer | b: result} + end + + defp bst(computer, operand) do + result = Integer.mod(eval_combo(computer, operand), 8) + %{computer | b: result} + end + + defp jnz(computer, operand) do + if computer.a == 0 do + advance_ip(computer) + else + %{computer | ip: operand} + end + end + + defp bxc(computer) do + result = Bitwise.bxor(computer.b, computer.c) + %{computer | b: result} + end + + defp out(computer, operand) do + result = Integer.mod(eval_combo(computer, operand), 8) + %{computer | output: [result | computer.output]} + end + + defp bdv(computer, operand) do + d = Integer.pow(2, eval_combo(computer, operand)) + %{computer | b: Integer.floor_div(computer.a, d)} + end + + defp cdv(computer, operand) do + d = Integer.pow(2, eval_combo(computer, operand)) + %{computer | c: Integer.floor_div(computer.a, d)} + end + + defp advance_ip(computer), do: %{computer | ip: computer.ip + 2} + + def part2(file) do + {computer, codes} = parse(file) + + # The program just loops dividing by at so we can solve subsets and multiply by + find_magic_A(computer, codes, codes) + end + + defp find_magic_A(computer, codes, target) do + a = + if(length(target) == 1) do + 0 + else + 8 * find_magic_A(computer, codes, tl(target)) + end + + + + Stream.iterate(a, &(&1 + 1)) + |> Enum.find(fn a -> + result = Enum.reverse(evaluate(%{computer | a: a}, codes).output) + result == target + end) + end +end + +file = hd(System.argv()) +IO.puts("part 1") +Day17.part1(file) |> IO.inspect() +IO.puts("part 2") +Day17.part2(file) |> IO.inspect() diff --git a/2024/day18.exs b/2024/day18.exs new file mode 100644 index 0000000..06f580e --- /dev/null +++ b/2024/day18.exs @@ -0,0 +1,86 @@ +Code.require_file("./grid.ex") + +defmodule Day18 do + defmodule Move do + defstruct [:pos, :score] + end + + def parse(file) do + File.stream!(file) + |> Enum.map(fn line -> + [x, y] = String.trim(line) |> String.split(",") + {String.to_integer(x), String.to_integer(y)} + end) + end + + def part1(file, bytes, size) do + corrupted = parse(file) |> Enum.take(bytes) |> corrupted_map() + + search(corrupted, {size, size}, MapSet.new(), [%Move{pos: {0, 0}, score: 0}]) + end + + defp corrupted_map(bytes) do + for point <- bytes, into: %{} do + {point, true} + end + end + + defp search(_, _, _, []), do: -1 + + defp search(corrupted, finish, visited, [current | move_queue]) do + if(finish == current.pos) do + current.score + else + possible_moves = + for dir <- [Grid.up(), Grid.down(), Grid.left(), Grid.right()] do + %Move{pos: Grid.add(current.pos, dir), score: current.score + 1} + end + + possible_moves = + Enum.reject(possible_moves, fn move -> + move.pos in visited || corrupted[move.pos] || + outside?(move.pos, finish) + end) + + queue = + (possible_moves ++ move_queue) + |> Enum.sort_by(& &1.score) + + visited = + for move <- possible_moves, reduce: visited do + acc -> MapSet.put(acc, move.pos) + end + + search(corrupted, finish, visited, queue) + end + end + + defp outside?({x, y}, {max_x, max_y}), do: x < 0 || y < 0 || x > max_x || y > max_y + + def part2(file, start_byte, size) do + falling_bytes = parse(file) + + {i, _} = + Stream.iterate({start_byte, length(falling_bytes)}, fn {low, high} -> + mid = Integer.floor_div(low + high, 2) + corrupted = Enum.take(falling_bytes, mid) |> corrupted_map() + + found = search(corrupted, {size, size}, MapSet.new(), [%Move{pos: {0, 0}, score: 0}]) + + if(found > 0) do + {mid + 1, high} + else + {low, mid - 1} + end + end) + |> Enum.find(fn {low, high} -> low > high end) + + Enum.at(falling_bytes, i - 1) + end +end + +[file, bytes, size] = System.argv() +IO.puts("Part 1") +Day18.part1(file, String.to_integer(bytes), String.to_integer(size)) |> IO.inspect() +IO.puts("Part 2") +Day18.part2(file, String.to_integer(bytes), String.to_integer(size)) |> IO.inspect() diff --git a/2024/day19.exs b/2024/day19.exs new file mode 100644 index 0000000..6328129 --- /dev/null +++ b/2024/day19.exs @@ -0,0 +1,70 @@ +defmodule Memo do + use Agent + + def start_link() do + Agent.start_link(fn -> %{} end, name: __MODULE__) + end + + def get(k) do + Agent.get(__MODULE__, fn memo -> memo[k] end) + end + + def put(k, v) do + Agent.update(__MODULE__, fn memo -> Map.put(memo, k, v) end) + end +end + +defmodule Day19 do + defp parse(file) do + [patterns | designs] = File.stream!(file) |> Enum.map(&String.trim/1) |> Enum.to_list() + ps = String.split(patterns, ",") |> Enum.map(&String.trim/1) + {ps, Enum.drop(designs, 1)} + end + + def part1(file) do + {patterns, designs} = parse(file) + + for design <- designs, reduce: 0 do + acc -> + if(make_design(design, patterns) > 0) do + acc + 1 + else + acc + end + end + end + + def part2(file) do + {patterns, designs} = parse(file) + + for design <- designs, reduce: 0 do + acc -> + acc + make_design(design, patterns) + end + end + + defp make_design("", _), do: 1 + + defp make_design(design, patterns) do + cached = Memo.get(design) + + if(cached != nil) do + cached + else + res = + Enum.filter(patterns, &String.starts_with?(design, &1)) + |> Enum.map(&make_design(String.replace_prefix(design, &1, ""), patterns)) + |> Enum.sum() + + Memo.put(design, res) + res + end + end +end + +Memo.start_link() +[file] = System.argv() +IO.puts("Part 1") +Day19.part1(file) |> IO.inspect() +IO.puts("Part 2") +Day19.part2(file) |> IO.inspect() diff --git a/2024/day2.exs b/2024/day2.exs new file mode 100644 index 0000000..0608b90 --- /dev/null +++ b/2024/day2.exs @@ -0,0 +1,56 @@ +defmodule Day2 do + defp file(), do: "test/day2.txt" + + defp parse_lines() do + File.stream!(file()) + |> Stream.map(&String.trim/1) + |> Stream.map(fn line -> + Enum.map(String.split(line), fn c -> String.to_integer(c) end) + end) + |> Enum.to_list() + end + + def part1() do + lines = parse_lines() + + Enum.count(lines, fn line -> safe?(line) end) + end + + defp safe?(line) do + [first, second | _rest] = line + + cond do + first > second -> safe?(line, fn a, b -> a - b end) + first < second -> safe?(line, fn a, b -> b - a end) + true -> false + end + end + + defp safe?([_head | []], _differ) do + true + end + + defp safe?([head | tail], differ) do + diff = differ.(head, Enum.at(tail, 0)) + + cond do + diff > 0 and diff < 4 -> safe?(tail, differ) + true -> false + end + end + + def part2() do + lines = parse_lines() + + Enum.count(lines, fn line -> + if safe?(line) do + true + else + Enum.with_index(line) |> Enum.any?(fn {_n, i} -> safe?(List.delete_at(line, i)) end) + end + end) + end +end + +IO.puts("part 1 - " <> Integer.to_string(Day2.part1())) +IO.puts("part 2 - " <> Integer.to_string(Day2.part2())) diff --git a/2024/day20.exs b/2024/day20.exs new file mode 100644 index 0000000..761f470 --- /dev/null +++ b/2024/day20.exs @@ -0,0 +1,71 @@ +Code.require_file("./grid.ex") + +defmodule Day20 do + defmodule Move do + defstruct [:pos, :score] + end + + def solve(file, cheat_amount, target) do + grid = Grid.parse(file) + start = Grid.find(grid, &(&1 == "S")) + finish = Grid.find(grid, &(&1 == "E")) + track = Grid.as_map(grid, &(&1 == "." || &1 == "S" || &1 == "E")) + track = Map.put(track, :max_x, Grid.max_x(grid)) + track = Map.put(track, :max_y, Grid.max_y(grid)) + + distances = + search(track, finish, %{start => 0}, :queue.from_list([%Move{pos: start, score: 0}])) + + d = Map.to_list(distances) + + cheats = + for {{ix, iy}, di} <- d, {{jx, jy}, dj} <- d do + mh = abs(jx - ix) + abs(jy - iy) + + if mh <= cheat_amount && dj >= di + mh do + dj - (di + mh) + else + 0 + end + end + + Enum.filter(cheats, &(&1 >= target)) |> Enum.count() + end + + defp search(track, finish, visited, queue) do + if(:queue.is_empty(queue)) do + visited + else + {{_, current}, queue} = :queue.out(queue) + + possible_moves = + for dir <- [Grid.up(), Grid.down(), Grid.left(), Grid.right()] do + %Move{pos: Grid.add(current.pos, dir), score: current.score + 1} + end + + possible_moves = + Enum.reject(possible_moves, fn move -> + visited[move.pos] || !track[move.pos] || outside?(track, move.pos) + end) + + queue = + for move <- possible_moves, reduce: queue do + acc -> :queue.in(move, acc) + end + + visited = + for move <- possible_moves, into: visited do + {move.pos, move.score} + end + + search(track, finish, visited, queue) + end + end + + defp outside?(track, {x, y}), do: x < 0 || y < 0 || x > track.max_x || y > track.max_y +end + +[file, cheat_amount, target] = System.argv() + + +Day20.solve(file, String.to_integer(cheat_amount), String.to_integer(target)) |> IO.inspect() diff --git a/2024/day21.exs b/2024/day21.exs new file mode 100644 index 0000000..353b8c6 --- /dev/null +++ b/2024/day21.exs @@ -0,0 +1,131 @@ +Mix.install([{:memoize, "~> 1.4"}]) + +defmodule Day21 do + use Memoize + + defp make_numpad, + do: %{ + "0" => {1, 3}, + "1" => {0, 2}, + "2" => {1, 2}, + "3" => {2, 2}, + "4" => {0, 1}, + "5" => {1, 1}, + "6" => {2, 1}, + "7" => {0, 0}, + "8" => {1, 0}, + "9" => {2, 0}, + "A" => {2, 3} + } + + defp make_dir_pad, + do: %{ + "^" => {1, 0}, + "A" => {2, 0}, + "<" => {0, 1}, + "v" => {1, 1}, + ">" => {2, 1} + } + + defp parse(file) do + File.stream!(file) |> Enum.map(&String.trim/1) |> Enum.to_list() + end + + def solve(file, pads) do + for code <- parse(file), reduce: 0 do + acc -> + code_pushes = + Enum.chunk_every(["A" | String.graphemes(code)], 2, 1, :discard) + |> Enum.reduce(0, fn [start, finish], acc -> + acc + sequence(make_numpad(), start, finish, pads) + end) + + acc + code_pushes * value(code) + end + end + + defp value(code), do: String.slice(code, 0, 3) |> String.to_integer() + + defmemo sequence(keypad, current, finish, 0) do + # Base case is manhatten distance + {cx, cy} = keypad[current] + {fx, fy} = keypad[finish] + abs(fx - cx) + abs(fy - cy) + 1 + end + + defmemo sequence(keypad, current, finish, robots) do + pushes = + for moves <- possible_moves(keypad, current, finish) do + {_, _, pushes} = + Stream.iterate({"A", moves, 0}, fn + {_, [], _} -> + false + + {current, [next | rest], acc} -> + acc = acc + sequence(make_dir_pad(), current, next, robots - 1) + {next, rest, acc} + end) + |> Stream.take_while(&Function.identity/1) + |> Enum.at(-1) + + from_end = Enum.at(moves, -1) || "A" + pushes + sequence(make_dir_pad(), from_end, "A", robots - 1) + end + + Enum.min(pushes) + end + + def possible_moves(keypad, from, to) do + {cx, cy} = keypad[from] + {fx, fy} = keypad[to] + dx = fx - cx + dy = fy - cy + + moves = [] + + moves = + cond do + dy < 0 -> moves ++ List.duplicate("^", abs(dy)) + dy > 0 -> moves ++ List.duplicate("v", dy) + true -> moves + end + + moves = + cond do + dx < 0 -> moves ++ List.duplicate("<", abs(dx)) + dx > 0 -> moves ++ List.duplicate(">", dx) + true -> moves + end + + permutations(moves) + |> Enum.filter(&all_valid?(keypad, from, &1)) + |> Enum.uniq() + end + + defp all_valid?(keypad, from, moves) do + valid_pos = Map.values(keypad) + + Enum.scan(moves, keypad[from], fn move, {cx, cy} -> + case(move) do + "^" -> {cx, cy - 1} + "v" -> {cx, cy + 1} + "<" -> {cx - 1, cy} + ">" -> {cx + 1, cy} + end + end) + |> Enum.all?(&(&1 in valid_pos)) + end + + defp permutations([]), do: [[]] + + defp permutations(list) do + for elem <- list, rest <- permutations(list -- [elem]), do: [elem | rest] + end +end + +[file] = System.argv() + +IO.puts("Part 1") +Day21.solve(file, 2) |> IO.inspect() +IO.puts("Part 2") +Day21.solve(file, 25) |> IO.inspect() diff --git a/2024/day22.exs b/2024/day22.exs new file mode 100644 index 0000000..ea4a9ee --- /dev/null +++ b/2024/day22.exs @@ -0,0 +1,56 @@ +defmodule Day22 do + defp parse(file) do + File.stream!(file) |> Enum.map(&String.trim/1) |> Enum.map(&String.to_integer/1) + end + + def part1(file) do + for secret <- parse(file), reduce: 0 do + acc -> + {secret, _} = + Stream.iterate(secret, &evolve/1) + |> Stream.with_index() + |> Enum.find(fn {_, i} -> i == 2000 end) + + acc + secret + end + end + + def part2(file) do + for secret <- parse(file) do + Stream.iterate({secret, nil}, fn {secret, _} -> + new_secret = evolve(secret) + diff = last_dig(new_secret) - last_dig(secret) + {new_secret, diff} + end) + |> Enum.take(2000) + |> Enum.drop(1) + |> Enum.chunk_every(4, 1, :discard) + |> Enum.map(fn [_, _, _, {sec, _}] = chunk -> + changes = Enum.map(chunk, &elem(&1, 1)) + {changes, last_dig(sec)} + end) + |> Enum.uniq_by(fn {changes, _} -> changes end) + end + |> List.flatten() + |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) + |> Map.values() + |> Enum.map(&Enum.sum(&1)) + |> Enum.max() + end + + def last_dig(n), do: Integer.mod(n, 10) + + def evolve(secret) do + secret = Bitwise.bxor(secret * 64, secret) |> prune + secret = Bitwise.bxor(Integer.floor_div(secret, 32), secret) |> prune + Bitwise.bxor(secret * 2048, secret) |> prune + end + + defp prune(number), do: Integer.mod(number, 16_777_216) +end + +[file] = System.argv() +IO.puts("Part 1") +Day22.part1(file) |> IO.inspect() +IO.puts("Part 2") +Day22.part2(file) |> IO.inspect(charlists: :as_lists) diff --git a/2024/day23.exs b/2024/day23.exs new file mode 100644 index 0000000..f8778c6 --- /dev/null +++ b/2024/day23.exs @@ -0,0 +1,81 @@ +defmodule Day23 do + defp parse(file) do + for line <- File.stream!(file), reduce: %{} do + acc -> + [a, b] = + String.trim(line) + |> String.split("-") + + acc = Map.update(acc, a, MapSet.new([b]), &MapSet.put(&1, b)) + Map.update(acc, b, MapSet.new([a]), &MapSet.put(&1, a)) + end + end + + def part1(file) do + connections = parse(file) + + with_t = + Map.filter(connections, fn {k, v} -> + String.starts_with?(k, "t") || any_starts_with_t?(v) + end) + + for {k, v} <- Map.to_list(with_t), + [a, b] <- cartesian_prod(v) do + if Enum.member?(Map.get(with_t, a, []), b) do + MapSet.new([k, a, b]) + end + end + |> Enum.filter(&Function.identity/1) + |> Enum.filter(&any_starts_with_t?/1) + |> Enum.uniq() + |> Enum.count() + end + + def cartesian_prod(vals) do + for a <- vals, b <- vals do + [a, b] + end + end + + defp any_starts_with_t?(nodes), do: Enum.any?(nodes, &String.starts_with?(&1, "t")) + + def part2(file) do + connections = parse_and_dedup(file) + + Stream.flat_map(Map.keys(connections), &search(connections, [&1], Enum.count(connections))) + |> Enum.max_by(&Enum.count(&1)) + |> Enum.reverse() + |> Enum.join(",") + end + + def search(_, nodes, 0), do: [nodes] + + def search(connections, [node | nodes], n) do + neighbors = + Map.get(connections, node, MapSet.new()) + |> Enum.filter(fn candidate -> + Enum.all?(nodes, &MapSet.member?(connections[&1], candidate)) + end) + + case neighbors do + [] -> [[node | nodes]] + candidates -> Enum.flat_map(candidates, &search(connections, [&1, node | nodes], n - 1)) + end + end + + def parse_and_dedup(file) do + File.stream!(file) + |> Enum.map(&String.trim/1) + |> Enum.map(&String.split(&1, "-")) + |> Enum.map(fn [a, b] -> if a < b, do: {a, b}, else: {b, a} end) + |> Enum.reduce(%{}, fn {a, b}, connections -> + Map.update(connections, a, MapSet.new([b]), &MapSet.put(&1, b)) + end) + end +end + +IO.puts("Part 1") +[file] = System.argv() +Day23.part1(file) |> IO.inspect() +IO.puts("Part 2") +Day23.part2(file) |> IO.inspect() diff --git a/2024/day24.exs b/2024/day24.exs new file mode 100644 index 0000000..e7b07c6 --- /dev/null +++ b/2024/day24.exs @@ -0,0 +1,192 @@ +defmodule Day24 do + defmodule Gate do + defstruct [:l, :r, :op, :dest] + end + + defp parse(file) do + {wires, instructions} = + File.stream!(file) |> Stream.map(&String.trim/1) |> Enum.split_while(&(&1 != "")) + + circuit = + for wire <- wires, into: %{} do + [name, value] = String.split(wire, ":") + {name, String.to_integer(String.trim(value))} + end + + gates = + for i <- Enum.drop(instructions, 1) do + [left, right] = String.split(i, "->") + [l, op, r] = String.split(left) + %Gate{l: l, op: String.to_atom(op), r: r, dest: String.trim(right)} + end + + {circuit, gates} + end + + def part1(file) do + {circuit, gates} = parse(file) + circuit = sim(circuit, gates) + + Map.keys(circuit) + |> Enum.filter(&String.starts_with?(&1, "z")) + |> Enum.sort() + |> Enum.map(&to_string(circuit[&1])) + |> Enum.join() + |> String.reverse() + |> String.to_integer(2) + end + + defp sim(circuit, []), do: circuit + + defp sim(circuit, gates) do + to_run = Enum.find(gates, &(Map.has_key?(circuit, &1.l) && Map.has_key?(circuit, &1.r))) + + circuit = + case(to_run.op) do + :AND -> do_op(circuit, to_run, &Bitwise.band/2) + :OR -> do_op(circuit, to_run, &Bitwise.bor/2) + :XOR -> do_op(circuit, to_run, &Bitwise.bxor/2) + end + + rest = List.delete(gates, to_run) + sim(circuit, rest) + end + + defp do_op(circuit, gate, op) do + l = circuit[gate.l] + r = circuit[gate.r] + res = op.(l, r) + Map.put(circuit, gate.dest, res) + end + + def part2(file) do + {_, gates} = parse(file) + z_gates = Enum.filter(gates, &String.starts_with?(&1.dest, "z")) + + connected_gates = find_connected(gates, z_gates, []) + + # All gates that output z should be an XOR (other than the last one) + bad = + Enum.filter(z_gates, fn gate -> + gate.op != :XOR + end) + |> Enum.map(& &1.dest) + |> Enum.drop(-1) + + # all XOR that come from OR or XOR should go to z gates + badXOR = + Enum.filter(connected_gates, &(&1.op == :XOR)) + |> Enum.filter(fn gate -> + prev_l = prev_op(gates, gate.l) + prev_r = prev_op(gates, gate.r) + (prev_l == :XOR && prev_r == :OR) || (prev_l == :OR && prev_r == :XOR) + end) + |> Enum.map(& &1.dest) + + bad = bad ++ badXOR + + # Or gates after and gates + bad_OR_l = + Enum.filter(gates, &(&1.op == :OR)) + |> Enum.filter(fn gate -> + prev_op(gates, gate.l) != :AND + end) + |> Enum.map(& &1.l) + + bad = bad ++ bad_OR_l + + bad_OR_r = + Enum.filter(gates, &(&1.op == :OR)) + |> Enum.filter(fn gate -> + prev_op(gates, gate.r) != :AND + end) + |> Enum.map(& &1.r) + + bad = bad ++ bad_OR_r + + bad_AND_l = + Enum.filter(gates, &(&1.op == :AND)) + |> Enum.filter(fn gate -> + prev = previous_gate(gates, gate.l) + prev && prev.op == :AND && prev.l != "x00" && prev.r != "x00" + end) + |> Enum.map(& &1.l) + + bad = bad ++ bad_AND_l + + bad_AND_r = + Enum.filter(gates, &(&1.op == :AND)) + |> Enum.filter(fn gate -> + prev = previous_gate(gates, gate.r) + prev && prev.op == :AND && prev.l != "x00" && prev.r != "x00" + end) + |> Enum.map(& &1.r) + + bad = bad ++ bad_AND_r + + # each OR should be connected to AND + badOrs = + Enum.filter(gates, fn gate -> + gate.op == :OR && previous_gate(gates, gate.l).op != :AND + end) + |> Enum.map(& &1.l) + + bad = bad ++ badOrs + + badOrs = + Enum.filter(gates, fn gate -> + gate.op == :OR && previous_gate(gates, gate.r).op != :AND + end) + |> Enum.map(& &1.r) + + bad = bad ++ badOrs + + Enum.sort(bad) + |> Enum.uniq() + |> Enum.join(",") + end + + defp find_connected(_, [], acc), + do: acc |> Enum.uniq() |> Enum.reject(&String.starts_with?(&1.dest, "z")) + + defp find_connected(gates, [current | to_check], acc) do + next_l = previous_gate(gates, current.l) + next_r = previous_gate(gates, current.l) + + to_check = + if next_l do + [next_l | to_check] + else + to_check + end + + to_check = + if next_r do + [next_r | to_check] + else + to_check + end + + find_connected(gates, to_check, [current | acc]) + end + + defp prev_op(gates, code) do + found = Enum.find(gates, &(&1.dest == code)) + + if found do + found.op + else + nil + end + end + + defp previous_gate(gates, code) do + Enum.find(gates, &(&1.dest == code)) + end +end + +[file] = System.argv() +IO.puts("Part 1") +Day24.part1(file) |> IO.inspect() +IO.puts("Part 2") +Day24.part2(file) |> IO.puts() diff --git a/2024/day25.exs b/2024/day25.exs new file mode 100644 index 0000000..1a502c9 --- /dev/null +++ b/2024/day25.exs @@ -0,0 +1,36 @@ +defmodule Day25 do + defp parse(file) do + File.stream!(file) + |> Stream.map(&String.trim/1) + |> Stream.chunk_by(&(&1 == "")) + |> Stream.reject(&(&1 == [""])) + |> Enum.split_with(&(hd(&1) == "#####")) + end + + def part1(file) do + {locks, keys} = parse(file) + + for lock <- locks, key <- keys, reduce: 0 do + acc -> + if fits(lock, key) do + acc + 1 + else + acc + end + end + end + + defp fits(lock, key) do + overlap = + Enum.zip(lock, key) + |> Enum.any?(fn {l, k} -> + Enum.zip(String.graphemes(l), String.graphemes(k)) + |> Enum.any?(fn {ll, kk} -> ll == "#" && kk == "#" end) + end) + + !overlap + end +end + +[file] = System.argv() +Day25.part1(file) |> IO.inspect() diff --git a/2024/day3.exs b/2024/day3.exs new file mode 100644 index 0000000..c9ac712 --- /dev/null +++ b/2024/day3.exs @@ -0,0 +1,52 @@ +defmodule Day3 do + defp file(), do: "test/day3.txt" + + def part1() do + data = File.read!(file()) + + sumMul(data) + end + + defp sumMul(data) do + Regex.scan(~r/mul\(([0-9]+),([0-9]+)\)/, data, capture: :all_but_first) + |> Enum.map(fn [x, y] -> + String.to_integer(x) * String.to_integer(y) + end) + |> Enum.sum() + end + + def part2() do + data = File.read!(file()) + + matches = + Regex.scan(~r/mul\(([0-9]+),([0-9]+)\)|(don't\(\))|(do\(\))/, data, capture: :all_but_first) + + sum(matches, true, 0) + end + + defp sum([], _take?, acc), do: acc + + defp sum([match | tail], true, acc) do + case match do + [x, y] when length(match) == 2 -> + sum(tail, true, acc + String.to_integer(x) * String.to_integer(y)) + + ["", "", "", "do()"] -> + sum(tail, true, acc) + + ["", "", "don't()"] -> + sum(tail, false, acc) + end + end + + defp sum([match | tail], false, acc) do + case match do + ["", "", "", "do()"] -> sum(tail, true, acc) + _ -> sum(tail, false, acc) + end + end +end + +IO.puts("Part 1 - " <> Integer.to_string(Day3.part1())) + +IO.puts("Part 2 - " <> Integer.to_string(Day3.part2())) diff --git a/2024/day4.exs b/2024/day4.exs new file mode 100644 index 0000000..ef8e55c --- /dev/null +++ b/2024/day4.exs @@ -0,0 +1,169 @@ +defmodule Day4 do + defp load_grid() do + File.stream!("test/day4.txt") + |> Enum.to_list() + |> List.to_tuple() + end + + def part1() do + grid = load_grid() + IO.inspect(grid) + countXmas(grid, 0, 0, 0) + end + + defp at(grid, x, y) do + elem(grid, y) |> String.at(x) + end + + defp countXmas(grid, x, y, acc) do + maxY = tuple_size(grid) - 1 + maxX = String.length(elem(grid, 0)) - 1 + + cond do + y > maxY -> + acc + + x > maxX -> + countXmas(grid, 0, y + 1, acc) + + true -> + countFromPos = + left(grid, x, y) + right(grid, maxX, x, y) + up(grid, x, y) + down(grid, maxY, x, y) + + up_right(grid, maxX, x, y) + down_right(grid, maxX, maxY, x, y) + up_left(grid, x, y) + + down_left(grid, maxY, x, y) + + countXmas(grid, x + 1, y, acc + countFromPos) + end + end + + defp right(grid, maxX, x, y) do + cond do + x + 3 > maxX -> 0 + String.slice(elem(grid, y), x..(x + 3)) == "XMAS" -> 1 + true -> 0 + end + end + + defp left(grid, x, y) do + cond do + x - 3 < 0 -> 0 + String.reverse(String.slice(elem(grid, y), (x - 3)..x)) == "XMAS" -> 1 + true -> 0 + end + end + + defp up(grid, x, y) do + cond do + y - 3 < 0 -> + 0 + + at(grid, x, y) <> at(grid, x, y - 1) <> at(grid, x, y - 2) <> at(grid, x, y - 3) == "XMAS" -> + 1 + + true -> + 0 + end + end + + defp down(grid, maxY, x, y) do + cond do + y + 3 > maxY -> + 0 + + at(grid, x, y) <> at(grid, x, y + 1) <> at(grid, x, y + 2) <> at(grid, x, y + 3) == "XMAS" -> + 1 + + true -> + 0 + end + end + + defp up_right(grid, maxX, x, y) do + cond do + x + 3 > maxX || y - 3 < 0 -> + 0 + + at(grid, x, y) <> at(grid, x + 1, y - 1) <> at(grid, x + 2, y - 2) <> at(grid, x + 3, y - 3) == + "XMAS" -> + 1 + + true -> + 0 + end + end + + defp down_right(grid, maxX, maxY, x, y) do + cond do + x + 3 > maxX || y + 3 > maxY -> + 0 + + at(grid, x, y) <> at(grid, x + 1, y + 1) <> at(grid, x + 2, y + 2) <> at(grid, x + 3, y + 3) == + "XMAS" -> + 1 + + true -> + 0 + end + end + + defp down_left(grid, maxY, x, y) do + cond do + x - 3 < 0 || y + 3 > maxY -> + 0 + + at(grid, x, y) <> at(grid, x - 1, y + 1) <> at(grid, x - 2, y + 2) <> at(grid, x - 3, y + 3) == + "XMAS" -> + 1 + + true -> + 0 + end + end + + defp up_left(grid, x, y) do + cond do + x - 3 < 0 || y - 3 < 0 -> + 0 + + at(grid, x, y) <> at(grid, x - 1, y - 1) <> at(grid, x - 2, y - 2) <> at(grid, x - 3, y - 3) == + "XMAS" -> + 1 + + true -> + 0 + end + end + + def part2() do + grid = load_grid() + countMas(grid, 0, 0, 0) + end + + defp countMas(grid, x, y, acc) do + maxY = tuple_size(grid) - 1 + maxX = String.length(elem(grid, 0)) - 1 + + cond do + y + 2 > maxY -> acc + x + 2 > maxX -> countMas(grid, 0, y + 1, acc) + xMas?(grid, x, y) -> countMas(grid, x + 1, y, acc + 1) + true -> countMas(grid, x + 1, y, acc) + end + end + + defp xMas?(grid, x, y) do + topLeft = at(grid, x, y) + topRight = at(grid, x + 2, y) + middle = at(grid, x + 1, y + 1) + bottomLeft = at(grid, x, y + 2) + bottomRight = at(grid, x + 2, y + 2) + + cross1 = topLeft <> middle <> bottomRight + cross2 = topRight <> middle <> bottomLeft + + (cross1 == "MAS" || cross1 == "SAM") && (cross2 == "MAS" || cross2 == "SAM") + end +end + +IO.puts("Part 1 - " <> Integer.to_string(Day4.part1())) +IO.puts("Part 2 - " <> Integer.to_string(Day4.part2())) diff --git a/2024/day5.exs b/2024/day5.exs new file mode 100644 index 0000000..e65668c --- /dev/null +++ b/2024/day5.exs @@ -0,0 +1,77 @@ +defmodule Day5 do + defp file(), do: "test/day5.txt" + + def part1() do + {updates, rules} = parse() + Enum.reduce(updates, 0, countMiddleIfMatchesRules(rules)) + end + + defp parse() do + {rulesData, updatesWithSep} = + File.stream!(file()) + |> Stream.map(&String.trim/1) + |> Enum.split_while(fn line -> String.length(line) > 0 end) + + updates = + Enum.drop(updatesWithSep, 1) + |> Enum.map(&String.split(&1, ",")) + |> Enum.map(fn update -> Enum.map(update, &String.to_integer/1) end) + + rules = + Enum.map(rulesData, fn rule -> + [first, second] = String.split(rule, "|") + {String.to_integer(first), String.to_integer(second)} + end) + + {updates, rules} + end + + defp countMiddleIfMatchesRules(rules) do + fn update, acc -> + if(matchesRules(rules, update)) do + acc + mid(update) + else + acc + end + end + end + + defp mid(update) do + Enum.at(update, Integer.floor_div(length(update), 2)) + end + + defp matchesRules(rules, update) do + index = Enum.with_index(update) |> Enum.into(%{}) + + Enum.all?(rules, fn {l, r} -> + il = Map.get(index, l) + ir = Map.get(index, r) + il == nil || ir == nil || il < ir + end) + end + + def part2() do + {updates, rules} = parse() + Enum.reduce(updates, 0, countMiddleOfSorted(rules)) + end + + defp countMiddleOfSorted(rules) do + fn update, acc -> + if matchesRules(rules, update) do + acc + else + sorted = Enum.sort(update, sorter(rules)) + acc + mid(sorted) + end + end + end + + defp sorter(rules) do + fn a, b -> + Enum.any?(rules, fn {l, r} -> a == r && b == l end) + end + end +end + +IO.puts("Part 1 -> " <> Integer.to_string(Day5.part1())) +IO.puts("Part 2 -> " <> Integer.to_string(Day5.part2())) diff --git a/2024/day6.exs b/2024/day6.exs new file mode 100644 index 0000000..81d6bb1 --- /dev/null +++ b/2024/day6.exs @@ -0,0 +1,139 @@ +defmodule Day6 do + defp load_grid() do + File.stream!("test/day6.txt") + |> Enum.to_list() + |> Enum.map(&String.trim/1) + |> List.to_tuple() + end + + def part1() do + grid = load_grid() + start = find_start(grid) + + positions_until_leave(grid, start, {0, -1}, MapSet.new([start])) + |> MapSet.size() + |> IO.inspect() + end + + defp find_start(grid) do + find_start(grid, 0, 0) + end + + defp find_start(grid, x, y) do + cond do + x > max_x(grid) -> + find_start(grid, 0, y + 1) + + at(grid, x, y) == "^" -> + {x, y} + + true -> + find_start(grid, x + 1, y) + end + end + + defp max_x(grid) do + String.length(elem(grid, 0)) - 1 + end + + defp max_y(grid), do: tuple_size(grid) - 1 + + defp at(grid, x, y) do + elem(grid, y) |> String.at(x) + end + + defp positions_until_leave(grid, current, direction, visited) do + newPos = add_coord(current, direction) + {newX, newY} = newPos + + cond do + newX < 0 || newX > max_x(grid) -> + visited + + newY < 0 || newY > max_y(grid) -> + visited + + at(grid, newX, newY) == "#" -> + positions_until_leave(grid, current, turn_right(direction), visited) + + true -> + positions_until_leave(grid, newPos, direction, MapSet.put(visited, newPos)) + end + end + + defp add_coord({ax, ay}, {bx, by}) do + {ax + bx, ay + by} + end + + defp turn_right(coord) do + case coord do + # right to down + {1, 0} -> {0, 1} + # down to left + {0, 1} -> {-1, 0} + # left to up + {-1, 0} -> {0, -1} + # up to right + {0, -1} -> {1, 0} + end + end + + def part2() do + grid = load_grid() + start = find_start(grid) + + potential_blockages = + positions_until_leave(grid, start, {0, -1}, MapSet.new([start])) |> Enum.to_list() + + find_loops(grid, start, potential_blockages, 0) |> IO.inspect() + end + + defp find_loops(_grid, _start, [], acc), do: acc + + defp find_loops(grid, start, [head | tail], acc) do + cond do + is_loop?(grid, start, head) -> find_loops(grid, start, tail, acc + 1) + true -> find_loops(grid, start, tail, acc) + end + end + + defp is_loop?(grid, start, {x, y}) do + current_line = elem(grid, y) + + new_line = + current_line + |> String.graphemes() + |> put_in([Access.at(x)], "#") + |> Enum.join() + + grid_with_extra = put_elem(grid, y, new_line) + is_loop?(grid_with_extra, start, {0, -1}, MapSet.new([{start, {0, -1}}])) + end + + defp is_loop?(grid, current, direction, visited) do + newPos = add_coord(current, direction) + {newX, newY} = newPos + + cond do + newX < 0 || newX > max_x(grid) -> + false + + newY < 0 || newY > max_y(grid) -> + false + + Enum.member?(visited, {newPos, direction}) -> + true + + at(grid, newX, newY) == "#" -> + is_loop?(grid, current, turn_right(direction), visited) + + true -> + is_loop?(grid, newPos, direction, MapSet.put(visited, {newPos, direction})) + end + end +end + +IO.puts("part 1") +Day6.part1() +IO.puts("part 2") +Day6.part2() diff --git a/2024/day7.exs b/2024/day7.exs new file mode 100644 index 0000000..9db74cc --- /dev/null +++ b/2024/day7.exs @@ -0,0 +1,59 @@ +defmodule Day7 do + defp parse_terms() do + File.stream!("test/day7.txt") + |> Enum.map(&String.trim/1) + |> Enum.map(fn line -> + [l, r] = String.split(line, ":") + rs = String.split(r) |> Enum.map(&String.to_integer/1) + {String.to_integer(l), rs} + end) + end + + def solve(available_ops) do + terms = parse_terms() + + for {l, rs} <- terms, reduce: 0 do + acc -> + case possible_to_calc?(l, rs, available_ops) do + true -> acc + l + false -> acc + end + end + end + + defp possible_to_calc?(l, rs, available_ops), do: possible?(l, Enum.reverse(rs), available_ops) + + defp possible?(l, [r], _available_ops), do: l == r + + defp possible?(l, [r | rs], available_ops) do + Enum.reject(available_ops, fn op -> !possible_op?(op, l, r) end) + |> Enum.any?(fn op -> possible?(reverse_apply(op, l, r), rs, available_ops) end) + end + + defp possible_op?(op, l, r) do + case(op) do + :+ -> l > r + :* -> l > 0 && rem(l, r) == 0 + :|| -> l > r && String.ends_with?(to_string(l), to_string(r)) + end + end + + defp reverse_apply(op, l, r) do + case(op) do + :+ -> + l - r + + :* -> + Integer.floor_div(l, r) + + :|| -> + String.replace_suffix(to_string(l), to_string(r), "") |> String.to_integer() + end + end +end + +IO.puts("Part 1") +Day7.solve([:+, :*]) |> IO.inspect() + +IO.puts("Part 2") +Day7.solve([:+, :*, :||]) |> IO.inspect() diff --git a/2024/day8.exs b/2024/day8.exs new file mode 100644 index 0000000..500ee6c --- /dev/null +++ b/2024/day8.exs @@ -0,0 +1,100 @@ +defmodule Day8 do + defp load_grid() do + File.stream!("test/day8.txt") + |> Enum.to_list() + |> Enum.map(&String.trim/1) + end + + defp build_point_map(grid) do + for {row, y} <- Enum.with_index(grid), + {val, x} <- String.graphemes(row) |> Enum.with_index(), + reduce: %{} do + acc -> + cond do + val == "." -> + acc + + Map.has_key?(acc, val) -> + elem(Map.get_and_update(acc, val, fn coords -> {coords, coords ++ [{x, y}]} end), 1) + + true -> + Map.put(acc, val, [{x, y}]) + end + end + end + + def part1() do + grid = load_grid() + types_to_points = build_point_map(grid) + maxX = max_x(grid) + maxY = max_y(grid) + + solve(types_to_points, fn {_type, pairs} -> + for {{ax, ay}, {bx, by}} <- pairs do + diffX = ax - bx + diffY = ay - by + + [{ax + diffX, ay + diffY}, {bx - diffX, by - diffY}] + |> Enum.reject(fn {x, y} -> x < 0 || x > maxX || y < 0 || y > maxY end) + end + end) + end + + def part2() do + grid = load_grid() + types_to_points = build_point_map(grid) + maxX = max_x(grid) + maxY = max_y(grid) + + solve(types_to_points, fn {_type, pairs} -> + for {{ax, ay}, {bx, by}} <- pairs do + diffX = ax - bx + diffY = ay - by + + find_nodes({ax, ay}, {diffX, diffY}, maxX, maxY, []) ++ + find_nodes({bx, by}, {-diffX, -diffY}, maxX, maxY, []) + end + end) + end + + defp solve(types_to_points, calcfn) do + Map.to_list(types_to_points) + |> Enum.map(fn {type, locs} -> + {type, all_pairs(locs)} + end) + |> Enum.flat_map(calcfn) + |> List.flatten() + |> Enum.uniq() + |> Enum.count() + |> IO.inspect() + end + + defp find_nodes({x, y}, {diffX, diffY}, maxX, maxY, acc) do + cond do + x < 0 || x > maxX -> acc + y < 0 || y > maxY -> acc + true -> find_nodes({x + diffX, y + diffY}, {diffX, diffY}, maxX, maxY, [{x, y}] ++ acc) + end + end + + defp all_pairs(locs) do + for i <- locs, + j <- locs do + if(i != j) do + {i, j} + end + end + end + + defp max_x(grid) do + String.length(Enum.at(grid, 0)) - 1 + end + + defp max_y(grid), do: length(grid) - 1 +end + +IO.puts("Part 1") +Day8.part1() + +IO.puts("Part 2") +Day8.part2() diff --git a/2024/day9.exs b/2024/day9.exs new file mode 100644 index 0000000..841ff0e --- /dev/null +++ b/2024/day9.exs @@ -0,0 +1,121 @@ +defmodule Day9 do + defp read_file() do + File.read!("test/day9.txt") + |> String.trim() + |> String.graphemes() + |> Enum.with_index() + end + + def part1() do + data = read_file() + + fs = + for {block, index} <- data do + size = String.to_integer(block) + + if(rem(index, 2) == 0) do + List.duplicate({:file, Integer.floor_div(index, 2)}, size) + else + List.duplicate({:free}, size) + end + end + |> List.flatten() + + frag(fs, []) + |> Enum.reverse() + |> Enum.with_index() + |> Enum.reduce(0, fn {{:file, id}, idx}, acc -> acc + id * idx end) + end + + defp frag([], acc), do: acc + + defp frag([{:file, id} | fs], acc), do: frag(fs, [{:file, id} | acc]) + + defp frag([{:free} | fs], acc) do + last = List.last(fs) + + case last do + {:file, id} -> frag([{:file, id} | Enum.drop(fs, -1)], acc) + {:free} -> frag([{:free} | Enum.drop(fs, -1)], acc) + nil -> acc + end + end + + def part2() do + data = read_file() + + {_, fs} = + for {block, index} <- data, reduce: {0, []} do + {block_start, acc} -> + size = String.to_integer(block) + + if(rem(index, 2) == 0) do + {block_start + size, + [ + %{type: :file, size: size, index: Integer.floor_div(index, 2), start: block_start} + | acc + ]} + else + {block_start + size, [%{type: :free, size: size, start: block_start} | acc]} + end + end + + compressed = + for chunk <- fs, reduce: Enum.reverse(fs) do + acc -> + cond do + chunk.type == :file -> + try_move(chunk, acc) + + true -> + acc + end + end + + Enum.sort_by(compressed, fn chunk -> chunk.start end) + |> Enum.reject(fn chunk -> chunk.size == 0 end) + |> Enum.reduce(0, fn chunk, acc -> + acc + total_score(chunk, chunk.size + chunk.start - 1, 0, chunk.size) + end) + end + + defp total_score(%{type: :free}, _, _, _), do: 0 + defp total_score(_, _, acc, 0), do: acc + + defp total_score(chunk, block_index, acc, remaining) do + total_score( + chunk, + block_index - 1, + acc + chunk.index * block_index, + remaining - 1 + ) + end + + defp try_move(chunk, fs) do + available_space = + Enum.find_index(fs, fn c -> + c.type == :free && c.size >= chunk.size && c.start < chunk.start + end) + + if(available_space) do + with_chunk_removed = List.delete(fs, chunk) + space = Enum.at(fs, available_space) + + List.update_at(with_chunk_removed, available_space, fn space -> + %{type: :file, size: chunk.size, index: chunk.index, start: space.start} + end) + |> List.insert_at(available_space + 1, %{ + type: :free, + size: space.size - chunk.size, + start: space.start + chunk.size + }) + else + fs + end + end +end + +IO.puts("part 1") +Day9.part1() |> IO.inspect() +IO.puts("part 2") +Day9.part2() |> IO.inspect() diff --git a/2024/grid.ex b/2024/grid.ex new file mode 100644 index 0000000..2185d3c --- /dev/null +++ b/2024/grid.ex @@ -0,0 +1,84 @@ +defmodule Grid do + def parse(file) do + parse(file, &Function.identity/1) + end + + def parse(file, element_fun) do + File.stream!(file) + |> parse_lines(element_fun) + end + + def parse_lines(lines, element_fun) do + Enum.map(lines, fn line -> + String.trim(line) + |> String.graphemes() + |> Enum.map(element_fun) + |> List.to_tuple() + end) + |> List.to_tuple() + end + + def at(grid, {x, y}), do: elem(grid, y) |> elem(x) + + def max_x(grid), do: tuple_size(elem(grid, 0)) - 1 + + def max_y(grid), do: tuple_size(grid) - 1 + + def add({ax, ay}, {bx, by}), do: {ax + bx, ay + by} + + def contains?(grid, {x, y}), do: x >= 0 and x <= max_x(grid) && y >= 0 && y <= max_y(grid) + + def up(), do: {0, -1} + def down(), do: {0, 1} + def left(), do: {-1, 0} + def right(), do: {1, 0} + def up(pos), do: add(pos, up()) + def down(pos), do: add(pos, down()) + def left(pos), do: add(pos, left()) + def right(pos), do: add(pos, right()) + + def neighbours(pos), do: [up(pos), down(pos), left(pos), right(pos)] + + def neighbours(grid, pos), do: neighbours(pos) |> Enum.filter(&contains?(grid, &1)) + + def as_map(grid), do: as_map(grid, &Function.identity/1) + + def as_map(grid, terrain_fn) do + for( + x <- 0..Grid.max_x(grid), + y <- 0..Grid.max_y(grid), + into: %{} + ) do + {{x, y}, terrain_fn.(Grid.at(grid, {x, y}))} + end + end + + def find(grid, matcher) do + do_find(Tuple.to_list(grid) |> Enum.with_index(), matcher) + end + + defp do_find([], _), do: nil + + defp do_find([{row, y} | tail], matcher) do + match = Tuple.to_list(row) |> Enum.with_index() |> Enum.find(&matcher.(elem(&1, 0))) + + if match do + {_, x} = match + {x, y} + else + do_find(tail, matcher) + end + end + + def set(grid, {x, y}, value) do + new_row = elem(grid, y) |> put_elem(x, value) + put_elem(grid, y, new_row) + end + + def print(grid) do + Tuple.to_list(grid) + |> Enum.each(fn line -> + Tuple.to_list(line) |> Enum.join() |> IO.puts() + end) + end +end