Skip to content

Commit 2b60c9e

Browse files
committed
Solve 2025/7/8
1 parent e934882 commit 2b60c9e

10 files changed

Lines changed: 1467 additions & 38 deletions

File tree

lib/2025/day_07.ex

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
defmodule AdventOfCode.Y2025.Day07 do
2+
@moduledoc """
3+
--- Day 7: Laboratories ---
4+
Problem Link: https://adventofcode.com/2025/day/7
5+
Difficulty: l
6+
Tags: grid dynamic-programming
7+
"""
8+
alias AdventOfCode.Helpers.{InputReader, Transformers}
9+
alias Yog
10+
11+
def input, do: InputReader.read_from_file(2025, 7)
12+
13+
@spec run(binary()) :: {non_neg_integer(), any()}
14+
def run(input \\ input()) do
15+
input = parse(input)
16+
17+
{run_1(input), run_2(input)}
18+
end
19+
20+
def parse(data \\ input()) do
21+
lines = Transformers.lines(data)
22+
23+
for {line, r} <- Enum.with_index(lines),
24+
{char, c} <- Enum.with_index(String.graphemes(line)),
25+
into: %{} do
26+
{{r, c}, char}
27+
end
28+
end
29+
30+
def run_1(grid) do
31+
graph = build_graph(grid)
32+
start = find_start(grid)
33+
34+
reached =
35+
Yog.Traversal.Walk.fold_walk(
36+
over: graph,
37+
from: start,
38+
using: :breadth_first,
39+
initial: MapSet.new(),
40+
with: fn acc, node, _meta ->
41+
{:continue, MapSet.put(acc, node)}
42+
end
43+
)
44+
45+
reached
46+
|> Enum.count(fn
47+
{r, c} -> Map.get(grid, {r, c}) == "^"
48+
_ -> false
49+
end)
50+
end
51+
52+
def run_2(grid) do
53+
graph = build_graph(grid)
54+
start = find_start(grid)
55+
max_r = get_max_r(grid)
56+
57+
case Yog.Traversal.Sort.topological_sort(graph) do
58+
{:ok, sorted} ->
59+
counts =
60+
Enum.reduce(sorted, %{start => 1}, fn node, acc ->
61+
case Map.get(acc, node) do
62+
nil ->
63+
acc
64+
65+
count ->
66+
Yog.successor_ids(graph, node)
67+
|> Enum.reduce(acc, fn succ, inner_acc ->
68+
Map.update(inner_acc, succ, count, &(&1 + count))
69+
end)
70+
end
71+
end)
72+
73+
counts
74+
|> Enum.filter(fn {{r, _}, _} -> r > max_r end)
75+
|> Enum.reduce(0, fn {_, n}, sum -> sum + n end)
76+
77+
{:error, :contains_cycle} ->
78+
0
79+
end
80+
end
81+
82+
defp get_max_r(grid), do: grid |> Map.keys() |> Enum.map(fn {r, _} -> r end) |> Enum.max()
83+
84+
defp find_start(grid) do
85+
Enum.find_value(grid, fn {pos, char} ->
86+
if char == "S", do: pos
87+
end)
88+
end
89+
90+
defp build_graph(grid) do
91+
Enum.reduce(grid, Yog.directed(), fn {{r, c}, char}, graph ->
92+
case char do
93+
"^" ->
94+
graph
95+
|> Yog.add_edge_ensure({r, c}, {r + 1, c - 1}, 1, char)
96+
|> Yog.add_edge_ensure({r, c}, {r + 1, c + 1}, 1, char)
97+
98+
_ ->
99+
Yog.add_edge_ensure(graph, {r, c}, {r + 1, c}, 1, char)
100+
end
101+
end)
102+
end
103+
end

lib/2025/day_08.ex

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
defmodule AdventOfCode.Y2025.Day08 do
2+
@moduledoc """
3+
--- Day 8: Playground ---
4+
Problem Link: https://adventofcode.com/2025/day/8
5+
Difficulty: s
6+
Tags: union-find
7+
"""
8+
alias AdventOfCode.Helpers.{InputReader, Transformers}
9+
alias Yog.DisjointSet
10+
11+
def input, do: InputReader.read_from_file(2025, 8)
12+
13+
def run(input \\ input()) do
14+
coords = parse(input)
15+
edges = build_sorted_edges(coords)
16+
17+
{run_1(coords, edges), run_2(coords, edges)}
18+
end
19+
20+
def parse(data \\ input()) do
21+
data
22+
|> Transformers.lines()
23+
|> Enum.map(fn line ->
24+
line
25+
|> String.split(",")
26+
|> Enum.map(&String.to_integer/1)
27+
|> List.to_tuple()
28+
end)
29+
|> List.to_tuple()
30+
end
31+
32+
def build_sorted_edges(coords) do
33+
n = tuple_size(coords)
34+
35+
for i <- 0..(n - 2),
36+
j <- (i + 1)..(n - 1) do
37+
{i, j, squared_dist(elem(coords, i), elem(coords, j))}
38+
end
39+
|> Enum.sort_by(fn {_, _, d} -> d end)
40+
end
41+
42+
defp squared_dist({x1, y1, z1}, {x2, y2, z2}) do
43+
dx = x1 - x2
44+
dy = y1 - y2
45+
dz = z1 - z2
46+
dx * dx + dy * dy + dz * dz
47+
end
48+
49+
def run_1(coords, edges \\ nil) do
50+
edges = edges || build_sorted_edges(coords)
51+
n = tuple_size(coords)
52+
dsu = Enum.reduce(0..(n - 1), DisjointSet.new(), fn i, acc -> DisjointSet.add(acc, i) end)
53+
54+
edges
55+
|> Enum.take(1000)
56+
|> Enum.reduce(dsu, fn {i, j, _}, acc ->
57+
DisjointSet.union(acc, i, j)
58+
end)
59+
|> DisjointSet.to_lists()
60+
|> Enum.map(&length/1)
61+
|> Enum.sort(:desc)
62+
|> Enum.take(3)
63+
|> Enum.product()
64+
end
65+
66+
def run_2(coords, edges \\ nil) do
67+
edges = edges || build_sorted_edges(coords)
68+
n = tuple_size(coords)
69+
dsu = Enum.reduce(0..(n - 1), DisjointSet.new(), fn i, acc -> DisjointSet.add(acc, i) end)
70+
71+
find_last_edge(edges, dsu, n, coords)
72+
end
73+
74+
defp find_last_edge([{i, j, _} | rest], dsu, num_circuits, coords) do
75+
{dsu1, root_i} = DisjointSet.find(dsu, i)
76+
{dsu2, root_j} = DisjointSet.find(dsu1, j)
77+
78+
if root_i != root_j do
79+
if num_circuits == 2 do
80+
{x1, _, _} = elem(coords, i)
81+
{x2, _, _} = elem(coords, j)
82+
x1 * x2
83+
else
84+
dsu3 = DisjointSet.union(dsu2, i, j)
85+
find_last_edge(rest, dsu3, num_circuits - 1, coords)
86+
end
87+
else
88+
find_last_edge(rest, dsu2, num_circuits, coords)
89+
end
90+
end
91+
end

lib/advent_of_code.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ defmodule AdventOfCode do
22
@moduledoc """
33
Module that solves problem given year and day.
44
"""
5-
@latest_year 2024
5+
@latest_year 2025
66

7-
@type year() :: 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024
7+
@type year() :: 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025
88
@type day() :: pos_integer()
99

1010
@doc """

lib/helpers/generator.ex

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,64 +16,81 @@ defmodule AdventOfCode.Helpers.Generator do
1616
- Test cases containing default tests for `run_1` and `run_2` at `test/<year>/day_<day>_test.exs` file
1717
"""
1818
def run({year, day}) do
19-
# Write the input data at `priv/input_files`
20-
input_file_path =
21-
:advent_of_code
22-
|> :code.priv_dir()
23-
|> Path.join("input_files")
24-
|> Path.join("#{year}_#{day}.txt")
19+
padded_day = zero_padded(day)
2520

26-
input_file =
27-
input_file_path
28-
|> create_input_file(year, day)
21+
# Write the input data at `priv/input_files`
22+
input_path = "priv/input_files/#{year}_#{day}.txt"
23+
input_result = create_input_file(input_path, year, day)
2924

3025
# Write code files at `lib/<year>/day_<day>.ex`
3126
code_content =
3227
@code_template
3328
|> EEx.eval_file(day: day, year: year, title: get_title(year, day))
3429

35-
code_file =
36-
"lib/#{year}/day_#{zero_padded(day)}.ex"
37-
|> create_file(code_content)
30+
code_path = "lib/#{year}/day_#{padded_day}.ex"
31+
code_result = create_file(code_path, code_content)
3832

3933
# Write test files at `test/<year>/day_<year>_test.exs`
4034
test_content =
4135
@test_template
4236
|> EEx.eval_file(day: day, year: year)
4337

44-
test_file =
45-
"test/#{year}/day_#{zero_padded(day)}_test.exs"
46-
|> create_file(test_content)
38+
test_path = "test/#{year}/day_#{padded_day}_test.exs"
39+
test_result = create_file(test_path, test_content)
4740

48-
"INPUT: #{input_file}\tCODE: #{code_file}\tTEST: #{test_file}\n"
41+
[input_result, code_result, test_result]
4942
end
5043

5144
defp create_file(path, content) do
5245
if !File.exists?(path) do
53-
File.write(path, content)
46+
path |> Path.dirname() |> File.mkdir_p!()
47+
48+
case File.write(path, content) do
49+
:ok -> {:ok, path}
50+
{:error, reason} -> {:error, path, reason}
51+
end
52+
else
53+
{:exists, path}
5454
end
5555
end
5656

5757
defp fetch_cookie(year, day) do
58-
HTTPoison.start()
59-
60-
"https://adventofcode.com/#{year}/day/#{day}/input"
61-
|> HTTPoison.get([{"cookie", "session=#{System.get_env("COOKIE", "")}"}])
62-
|> then(fn response ->
63-
case response do
64-
{:ok, %HTTPoison.Response{body: body}} -> {:ok, body}
65-
_ -> nil
66-
end
67-
end)
58+
session_key = System.get_env("AOC_SESSION_KEY")
59+
60+
if is_nil(session_key) or session_key == "" do
61+
{:error, "AOC_SESSION_KEY environment variable is not set"}
62+
else
63+
HTTPoison.start()
64+
65+
"https://adventofcode.com/#{year}/day/#{day}/input"
66+
|> HTTPoison.get([{"cookie", "session=#{session_key}"}])
67+
|> then(fn response ->
68+
case response do
69+
{:ok, %HTTPoison.Response{status_code: 200, body: body}} -> {:ok, body}
70+
{:ok, %HTTPoison.Response{status_code: status}} -> {:error, "Received HTTP #{status}"}
71+
{:error, %HTTPoison.Error{reason: reason}} -> {:error, reason}
72+
_ -> {:error, "unknown error"}
73+
end
74+
end)
75+
end
6876
end
6977

7078
defp create_input_file(path, year, day) do
7179
if !File.exists?(path) do
72-
with {:ok, data} <- fetch_cookie(year, day),
73-
{:ok, file} <- File.open(path, [:write]),
74-
:ok <- IO.write(file, data) do
75-
File.close(file)
80+
path |> Path.dirname() |> File.mkdir_p!()
81+
82+
case fetch_cookie(year, day) do
83+
{:ok, data} ->
84+
case File.write(path, data) do
85+
:ok -> {:ok, path}
86+
{:error, reason} -> {:error, path, reason}
87+
end
88+
89+
{:error, reason} ->
90+
{:error, path, reason}
7691
end
92+
else
93+
{:exists, path}
7794
end
7895
end
7996

lib/mix/tasks/gen.ex

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ defmodule Mix.Tasks.Gen do
55
Type `mix gen --year <year> --day <day>` to create the boilerplate files for
66
solving the problem of the year <year> and day <day>.
77
8-
For example, `mix gen --year 2015 --day 2` will generate `lib/2015/day_2.ex`,
9-
`lib/data/inputs/2015_2.txt`, and `test/2015/day_2_test.exs` and contain the common
8+
For example, `mix gen --year 2015 --day 2` will generate `lib/2015/day_02.ex`,
9+
`priv/input_files/2015_2.txt`, and `test/2015/day_02_test.exs` and contain the common
1010
structure (i.e. module name, file reader function, test cases etc)
1111
"""
1212

@@ -25,7 +25,16 @@ defmodule Mix.Tasks.Gen do
2525
{year, day} ->
2626
{to_string(year), to_string(day)}
2727
|> Generator.run()
28-
|> Mix.shell().info()
28+
|> Enum.each(fn
29+
{:ok, path} ->
30+
Mix.shell().info([:green, "* created ", :reset, path])
31+
32+
{:exists, path} ->
33+
Mix.shell().info([:yellow, "* skipped ", :reset, path, " (already exists)"])
34+
35+
{:error, path, reason} ->
36+
Mix.shell().error([:red, "* error ", :reset, path, " (reason: #{inspect(reason)})"])
37+
end)
2938

3039
_ ->
3140
Mix.shell().error("[Usage] #{@usage}")

0 commit comments

Comments
 (0)