Skip to content

Commit

Permalink
2023 day 10 - refactor in a much more reasonable way
Browse files Browse the repository at this point in the history
  • Loading branch information
sevenseacat committed Dec 10, 2023
1 parent 53deb6e commit 601283a
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 110 deletions.
4 changes: 2 additions & 2 deletions lib/y2023/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ day 08, part 1 548.96 1.82 ms ±13.11% 1.80 ms 2.
day 08, part 2 246.36 4.06 ms ±4.48% 4.02 ms 4.64 ms
day 09, part 1 1618.02 0.62 ms ±81.29% 0.55 ms 2.27 ms
day 09, part 2 1603.46 0.62 ms ±70.45% 0.56 ms 2.45 ms
day 10, part 1 0.84 1194.74 ms ±4.76% 1183.27 ms 1257.72 ms
day 10, part 2 0.129 7780.62 ms ±0.00% 7780.62 ms 7780.62 ms
day 10, part 1 0.89 1124.28 ms ±2.76% 1128.01 ms 1152.94 ms
day 10, part 2 0.63 1593.22 ms ±7.67% 1555.84 ms 1769.60 ms
```
160 changes: 52 additions & 108 deletions lib/y2023/day10.ex
Original file line number Diff line number Diff line change
@@ -1,35 +1,63 @@
defmodule Y2023.Day10 do
use Advent.Day, no: 10

def part1({_raw, graph, start}) do
def part1({graph, start}) do
path = find_path(graph, start)
div(length(path), 4)
end

def part2({raw, graph, start}) do
def part2({graph, start}) do
# The number of nodes *inside* the loop is the total number of nodes, without
# the outside nodes and the loop nodes.
path = find_path(graph, start)
vertices = Graph.vertices(graph)

# Filter out all non-path pipes, so we're left with a map of %{coord => pipe_type}
# This includes replacing the S with the type of pipe it actually is
input =
raw
|> raw_input()
|> Enum.filter(fn {coord, _char} -> coord in path end)
|> Enum.into(%{})
|> Map.put(start, start_pipe(path))

{max_row, _} = vertices |> Enum.max_by(&elem(&1, 0))
max_row = ceil(max_row)
{_, max_col} = vertices |> Enum.max_by(&elem(&1, 1))
max_col = ceil(max_col)

for row <- 1..max_row,
col <- 1..max_col,
within_path?(input, path, {row, col}, {max_row, max_col}) do
{row, col}
{max_row, _} = Enum.max_by(vertices, &elem(&1, 0))
max_row = ceil(max_row) + 1
{_, max_col} = Enum.max_by(vertices, &elem(&1, 1))
max_col = ceil(max_col) + 1

is_int? = fn val -> trunc(val) == val end
set = Enum.map(path, fn {row, col} -> {row * 1.0, col * 1.0} end) |> MapSet.new()

# You can't do ranges stepping by 0.5?
# Instead, double the range and then half the values - now everything is a float
# Start from 0,0 (when row/col starts at 1) so we always have a guaranteed
# "outside" node to start from
# Step by 0.5s because we want to have paths going in between two pipes
non_path_graph =
for row <- 0..(max_row * 2), col <- 0..(max_col * 2) do
{row / 2, col / 2}
end
|> Enum.reduce(Graph.new(), fn {row, col}, graph ->
if MapSet.member?(set, {row, col}) do
graph
else
graph
|> Graph.add_vertex({row, col})
|> maybe_add_edge({row, col}, {row - 0.5, col})
|> maybe_add_edge({row, col}, {row, col - 0.5})
end
end)

outside =
Graph.reachable(non_path_graph, [{0.0, 0.0}])
|> Enum.filter(fn {row, col} ->
# Not really integers - just x.0 float values
is_int?.(row) && is_int?.(col)
end)

(max_row + 1) * (max_col + 1) - length(outside) - div(length(path), 2)
end

defp maybe_add_edge(graph, from, to) do
if Graph.has_vertex?(graph, to) do
graph
|> Graph.add_edge(from, to)
|> Graph.add_edge(to, from)
else
graph
end
|> length
end

defp find_path(graph, {row, col}) do
Expand All @@ -53,102 +81,18 @@ defmodule Y2023.Day10 do
[{row, col} | path]
end

defp start_pipe(path) do
# TODO: Find a way to make this not suck
{start_row, start_col} = hd(path)
{next_row, next_col} = Enum.at(path, 1)
{prev_row, prev_col} = List.last(path)

case {{start_row - prev_row, start_col - prev_col},
{next_row - start_row, next_col - start_col}} do
{{0, 0.5}, {-0.5, 0}} -> "J"
{{0.5, 0}, {0, -0.5}} -> "J"
{{0.5, 0}, {0, 0.5}} -> "L"
{{0, -0.5}, {-0.5, 0}} -> "L"
{{0, 0.5}, {0.5, 0}} -> "7"
{{-0.5, 0}, {0, -0.5}} -> "7"
{{-0.5, 0}, {0, 0.5}} -> "F"
{{0, -0.5}, {0.5, 0}} -> "F"
end
end

defp within_path?(raw_input, path, {row, col}, {max_row, max_col}) do
if {row, col} in path do
false
else
# Get the actual list of characters that make up the grid to the left,
# right, top and bottom of the coord in question
# There needs to be an odd number of pipes between the coord and the edge of the map
# Pipes could be simple and straight eg | or - but could be bent
# eg. - counts as a single pipe when looking from above/below the |
# |
# -
#
# | is a single pipe when looking right from X
# x ---
# |
# So regex over the string is the easiest way here?
left_row = get_coords(raw_input, row, 0..(col - 1))
right_row = get_coords(raw_input, row, (col + 1)..max_col)
up_col = get_coords(raw_input, 0..(row - 1), col)
down_col = get_coords(raw_input, (row + 1)..max_row, col)

row_match?(left_row) && row_match?(right_row) && col_match?(up_col) &&
col_match?(down_col)
end
end

defp get_coords(input, row_range, col_range) do
row_range = if is_integer(row_range), do: row_range..row_range, else: row_range
col_range = if is_integer(col_range), do: col_range..col_range, else: col_range

for row <- row_range, col <- col_range do
Map.get(input, {row, col}, ".")
end
|> Enum.join("")
end

defp row_match?(list), do: do_match?(list, ~r/(F-*J|L-*7|\|)/)
defp col_match?(list), do: do_match?(list, ~r/(7\|*L|F\|*J|\-)/)

defp do_match?(list, regex) do
matches =
Regex.scan(regex, list, capture: :all_but_first)
|> length()

rem(matches, 2) != 0
end

def raw_input(input) do
input
|> String.split("\n", trim: true)
|> Enum.reduce({%{}, 1}, &raw_row/2)
|> elem(0)
end

defp raw_row(row, {map, row_num}) do
{map, _col_num} =
row
|> String.graphemes()
|> Enum.reduce({map, 1}, fn char, {map, col_num} ->
{Map.put_new(map, {row_num, col_num}, char), col_num + 1}
end)

{map, row_num + 1}
end

def parse_input(input) do
input
|> String.split("\n", trim: true)
|> Enum.with_index(1)
|> Enum.reduce({input, Graph.new(), nil}, &parse_row/2)
|> Enum.reduce({Graph.new(), nil}, &parse_row/2)
end

defp parse_row({row, row_no}, acc) do
row
|> String.graphemes()
|> Enum.with_index(1)
|> Enum.reduce(acc, fn {char, col_no}, {input, graph, start} ->
|> Enum.reduce(acc, fn {char, col_no}, {graph, start} ->
position = {row_no, col_no}
joined_to = get_join_positions(char, position)
start = if char == "S", do: position, else: start
Expand All @@ -161,7 +105,7 @@ defmodule Y2023.Day10 do
|> Graph.add_edge(position, join)
end)

{input, graph, start}
{graph, start}
end)
end

Expand Down

0 comments on commit 601283a

Please sign in to comment.