diff --git a/src/AdventOfCode.Puzzles/2024/16/Part1/Part1.cs b/src/AdventOfCode.Puzzles/2024/16/Part1/Part1.cs new file mode 100644 index 0000000..19f76ee --- /dev/null +++ b/src/AdventOfCode.Puzzles/2024/16/Part1/Part1.cs @@ -0,0 +1,108 @@ +namespace AdventOfCode.Puzzles._2024._16.Part1; + +public partial class Part1 : IPuzzleSolution +{ + private int _width; + private int _height; + private char[,] _map; + private Point _start; + private Point _end; + + public record State(Point Position, Point Direction); + + public async Task SolveAsync(StreamReader inputReader) + { + await ReadInputAsync(inputReader); + + var lowestCost = FindLowestCost(); + return lowestCost.ToString(); + } + + private int FindLowestCost() + { + var states = new PriorityQueue(); + var minimumCosts = new Dictionary(); + var finalized = new HashSet(); + + var startingState = new State(_start, new(1, 0)); + states.Enqueue(startingState, 0); + minimumCosts.Add(startingState, 0); + + while (states.Count > 0) + { + var state = states.Dequeue(); + finalized.Add(state); + + // We go forward + var next = state.Position + state.Direction; + var nextState = new State(next, state.Direction); + AddNextIfImproved(nextState, minimumCosts[state] + 1); + + // We rotate clockwise + nextState = new State(state.Position, new Point(-state.Direction.Y, state.Direction.X)); + AddNextIfImproved(nextState, minimumCosts[state] + 1000); + + // We rotate counter-clockwise + nextState = new State(state.Position, new Point(state.Direction.Y, -state.Direction.X)); + AddNextIfImproved(nextState, minimumCosts[state] + 1000); + + void AddNextIfImproved(State nextState, int cost) + { + if (nextState.Position.X < 0 || nextState.Position.X >= _width || nextState.Position.Y < 0 || nextState.Position.Y >= _height) + { + return; + } + + if (_map[nextState.Position.X, nextState.Position.Y] == '#') + { + return; + } + + if (!finalized.Contains(nextState)) + { + if (!minimumCosts.TryGetValue(nextState, out var nextCost) || nextCost > cost) + { + minimumCosts[nextState] = cost; + states.Remove(nextState, out _, out _); + states.Enqueue(nextState, minimumCosts[nextState]); + } + } + } + } + + var bestEndState = int.MaxValue; + foreach (var direction in Directions.WithoutDiagonals) + { + bestEndState = Math.Min(bestEndState, minimumCosts.GetValueOrDefault(new State(_end, direction), int.MaxValue)); + } + + return bestEndState; + } + + private async Task ReadInputAsync(StreamReader inputReader) + { + var lines = await inputReader.ReadAllLinesAsync(); + _width = lines[0].Length; + _height = lines.Count; + _map = new char[_width, _height]; + + for (var y = 0; y < _height; y++) + { + var line = lines[y]; + for (var x = 0; x < _width; x++) + { + _map[x, y] = line[x]; + if (_map[x, y] == 'S') + { + _start = new(x, y); + _map[x, y] = '.'; + } + else if (_map[x, y] == 'E') + { + _end = new(x, y); + _map[x, y] = '.'; + } + } + } + } +} diff --git a/src/AdventOfCode.Puzzles/2024/16/Part2/Part2.cs b/src/AdventOfCode.Puzzles/2024/16/Part2/Part2.cs new file mode 100644 index 0000000..196c5c1 --- /dev/null +++ b/src/AdventOfCode.Puzzles/2024/16/Part2/Part2.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using AdventOfCode.Puzzles.Tools; + +namespace AdventOfCode.Puzzles._2024._16.Part2; + +public partial class Part2 : IPuzzleSolution +{ + private int _width; + private int _height; + private char[,] _map; + private Point _start; + private Point _end; + + public record State(Point Position, Point Direction); + + public async Task SolveAsync(StreamReader inputReader) + { + await ReadInputAsync(inputReader); + + var lowestCost = FindBestPathTileCount(); + return lowestCost.ToString(); + } + + private int FindBestPathTileCount() + { + var states = new PriorityQueue(); + var minimumCosts = new Dictionary(); + var finalized = new HashSet(); + + var sourceStates = new Dictionary>(); + + var startingState = new State(_start, new(1, 0)); + states.Enqueue(startingState, 0); + minimumCosts.Add(startingState, 0); + + while (states.Count > 0) + { + var state = states.Dequeue(); + finalized.Add(state); + + // We go forward + var next = state.Position + state.Direction; + var nextState = new State(next, state.Direction); + AddNextIfImproved(nextState, minimumCosts[state] + 1); + + // We rotate clockwise + nextState = new State(state.Position, new Point(-state.Direction.Y, state.Direction.X)); + AddNextIfImproved(nextState, minimumCosts[state] + 1000); + + // We rotate counter-clockwise + nextState = new State(state.Position, new Point(state.Direction.Y, -state.Direction.X)); + AddNextIfImproved(nextState, minimumCosts[state] + 1000); + + void AddNextIfImproved(State nextState, int cost) + { + if (nextState.Position.X < 0 || nextState.Position.X >= _width || nextState.Position.Y < 0 || nextState.Position.Y >= _height) + { + return; + } + + if (_map[nextState.Position.X, nextState.Position.Y] == '#') + { + return; + } + + if (!finalized.Contains(nextState)) + { + if (!minimumCosts.TryGetValue(nextState, out var nextCost) || nextCost > cost) + { + minimumCosts[nextState] = cost; + sourceStates[nextState] = new(); + states.Remove(nextState, out _, out _); + states.Enqueue(nextState, minimumCosts[nextState]); + } + + if (minimumCosts[nextState] == cost) + { + sourceStates[nextState].Add(state); + } + } + } + } + + var bestEndState = int.MaxValue; + foreach (var direction in Directions.WithoutDiagonals) + { + bestEndState = Math.Min(bestEndState, minimumCosts.GetValueOrDefault(new State(_end, direction), int.MaxValue)); + } + + var bestPathTiles = new HashSet(); + Queue backtrackQueue = new Queue(); + + foreach (var direction in Directions.WithoutDiagonals.Where(d => minimumCosts.GetValueOrDefault(new State(_end, d), int.MaxValue) == bestEndState)) + { + backtrackQueue.Enqueue(new State(_end, direction)); + } + + while (backtrackQueue.Count > 0) + { + var current = backtrackQueue.Dequeue(); + bestPathTiles.Add(current.Position); + + if (sourceStates.TryGetValue(current, out var sources)) + { + foreach (var source in sources) + { + backtrackQueue.Enqueue(source); + } + } + } + + return bestPathTiles.Count; + } + + private async Task ReadInputAsync(StreamReader inputReader) + { + var lines = await inputReader.ReadAllLinesAsync(); + _width = lines[0].Length; + _height = lines.Count; + _map = new char[_width, _height]; + + for (var y = 0; y < _height; y++) + { + var line = lines[y]; + for (var x = 0; x < _width; x++) + { + _map[x, y] = line[x]; + if (_map[x, y] == 'S') + { + _start = new(x, y); + _map[x, y] = '.'; + } + else if (_map[x, y] == 'E') + { + _end = new(x, y); + _map[x, y] = '.'; + } + } + } + } +} diff --git a/src/AdventOfCode.Puzzles/2024/16/TestData.txt b/src/AdventOfCode.Puzzles/2024/16/TestData.txt new file mode 100644 index 0000000..bc61c57 --- /dev/null +++ b/src/AdventOfCode.Puzzles/2024/16/TestData.txt @@ -0,0 +1,17 @@ +################# +#...#...#...#..E# +#.#.#.#.#.#.#.#.# +#.#.#.#...#...#.# +#.#.#.#.###.#.#.# +#...#.#.#.....#.# +#.#.#.#.#.#####.# +#.#...#.#.#.....# +#.#.#####.#.###.# +#.#.#.......#...# +#.#.###.#####.### +#.#.#...#.....#.# +#.#.#.#####.###.# +#.#.#.........#.# +#.#.#.#########.# +#S#.............# +################# diff --git a/src/AdventOfCode.Puzzles/GlobalUsings.cs b/src/AdventOfCode.Puzzles/GlobalUsings.cs new file mode 100644 index 0000000..3d745bf --- /dev/null +++ b/src/AdventOfCode.Puzzles/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.Text; +global using AdventOfCode.Puzzles.Tools;