diff --git a/src/AdventOfCode/Day17.cs b/src/AdventOfCode/Day17.cs new file mode 100644 index 0000000..9a1d91f --- /dev/null +++ b/src/AdventOfCode/Day17.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using AdventOfCode.Utilities; + +namespace AdventOfCode +{ + /// + /// Solver for Day 17 + /// + public class Day17 + { + public int Part1(string[] input) + { + LavaGrid grid = LavaGrid.Parse(input); + return grid.ShortestPath(0, 3); + } + + public int Part2(string[] input) + { + LavaGrid grid = LavaGrid.Parse(input); + return grid.ShortestPath(4, 10); + } + + /// + /// Lava grid + /// + /// Grid of tile costs + /// Grid width + /// Grid height + private record LavaGrid(int[,] Grid, int Width, int Height) + { + /// + /// Parse the grid + /// + /// Input + /// Lava grid + public static LavaGrid Parse(IReadOnlyList input) + { + int[,] grid = input.ToGrid(); + + return new LavaGrid(grid, input[0].Length, input.Count); + } + + /// + /// Calculate the shortest valid path from the top left of the grid to the bottom right + /// within the given constraints + /// + /// Minimum moves before we're allowed to make a turn + /// Maximum moves before we must make a turn + /// Shortest valid path + /// No path found + public int ShortestPath(int minimumMoves, int maximumMoves) + { + StepState startSouth = new((0, 1), Bearing.South, 1); + StepState startEast = new((1, 0), Bearing.East, 1); + Point2D target = (this.Width - 1, this.Height - 1); + + Dictionary distances = new() + { + [startSouth] = this.Grid[1, 0], + [startEast] = this.Grid[0, 1], + }; + + PriorityQueue queue = new(); + queue.Enqueue(startSouth, this.Grid[1, 0]); + queue.Enqueue(startEast, this.Grid[0, 1]); + + HashSet visited = new(); + + while (queue.Count > 0) + { + StepState current = queue.Dequeue(); + visited.Add(current); + + if (current.Point == target) + { + // guaranteed to be shortest because the queue is ordered by cost + return distances[current]; + } + + foreach (StepState next in NextMoves(current, minimumMoves, maximumMoves)) + { + if (!this.InBounds(next.Point)) + { + continue; + } + + if (visited.Contains(next)) + { + continue; + } + + int nextDistance = distances[current] + this.Grid[next.Point.Y, next.Point.X]; + + // only move if it's cheaper than the current best path + if (!distances.TryGetValue(next, out int currentBest) || nextDistance < currentBest) + { + distances[next] = nextDistance; + queue.Enqueue(next, nextDistance); + } + } + } + + throw new InvalidOperationException("No path found"); + } + + /// + /// Enumerable the possible next moves from the current state + /// + /// Current state + /// Minimum moves before we're allowed to make a turn + /// Maximum moves before we must make a turn + /// Valid next moves + private static IEnumerable NextMoves(StepState state, int minimumMoves, int maximumMoves) + { + if (state.Consecutive < minimumMoves) + { + yield return new StepState(state.Point.Move(state.Bearing), state.Bearing, state.Consecutive + 1); + + // not met minimum yet, so can't make any further moves + yield break; + } + + if (state.Consecutive < maximumMoves) + { + yield return new StepState(state.Point.Move(state.Bearing), state.Bearing, state.Consecutive + 1); + } + + switch (state.Bearing) + { + case Bearing.South or Bearing.North: + yield return new StepState(state.Point.Move(Bearing.West), Bearing.West, 1); + yield return new StepState(state.Point.Move(Bearing.East), Bearing.East, 1); + break; + + case Bearing.East or Bearing.West: + yield return new StepState(state.Point.Move(Bearing.North), Bearing.North, 1); + yield return new StepState(state.Point.Move(Bearing.South), Bearing.South, 1); + break; + } + } + + /// + /// Check if the given point is in bounds + /// + /// Point + /// Point is in bounds + private bool InBounds(Point2D point) => point.X >= 0 && point.X < this.Width + && point.Y >= 0 && point.Y < this.Height; + } + + /// + /// Step state + /// + /// Current point + /// Bearing taken to enter this point (to prevent backtracking, which is not allowed) + /// Number of consecutive steps taken on this bearing + private record StepState(Point2D Point, Bearing Bearing, int Consecutive); + } +} diff --git a/src/AdventOfCode/Utilities/GridUtilities.cs b/src/AdventOfCode/Utilities/GridUtilities.cs index a195cba..3e7e825 100644 --- a/src/AdventOfCode/Utilities/GridUtilities.cs +++ b/src/AdventOfCode/Utilities/GridUtilities.cs @@ -44,12 +44,12 @@ private static readonly (int x, int y)[] Deltas = /// /// Input lines /// Grid - public static T[,] ToGrid(this string[] input) + public static T[,] ToGrid(this IReadOnlyList input) { // y,x remember, not x,y - T[,] grid = new T[input.Length, input[0].Length]; + T[,] grid = new T[input.Count, input[0].Length]; - for (int y = 0; y < input.Length; y++) + for (int y = 0; y < input.Count; y++) { for (int x = 0; x < input[y].Length; x++) { diff --git a/src/AdventOfCode/inputs/day17.txt b/src/AdventOfCode/inputs/day17.txt new file mode 100644 index 0000000..20a4c86 Binary files /dev/null and b/src/AdventOfCode/inputs/day17.txt differ diff --git a/tests/AdventOfCode.Tests/Day17Tests.cs b/tests/AdventOfCode.Tests/Day17Tests.cs new file mode 100644 index 0000000..d010728 --- /dev/null +++ b/tests/AdventOfCode.Tests/Day17Tests.cs @@ -0,0 +1,86 @@ +using System.IO; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.Tests +{ + public class Day17Tests + { + private readonly ITestOutputHelper output; + private readonly Day17 solver; + + public Day17Tests(ITestOutputHelper output) + { + this.output = output; + this.solver = new Day17(); + } + + private static string[] GetRealInput() + { + string[] input = File.ReadAllLines("inputs/day17.txt"); + return input; + } + + private static string[] GetSampleInput() + { + return new string[] + { + "2413432311323", + "3215453535623", + "3255245654254", + "3446585845452", + "4546657867536", + "1438598798454", + "4457876987766", + "3637877979653", + "4654967986887", + "4564679986453", + "1224686865563", + "2546548887735", + "4322674655533", + }; + } + + [Fact] + public void Part1_SampleInput_ProducesCorrectResponse() + { + var expected = 102; + + var result = solver.Part1(GetSampleInput()); + + Assert.Equal(expected, result); + } + + [Fact] + public void Part1_RealInput_ProducesCorrectResponse() + { + var expected = 928; + + var result = solver.Part1(GetRealInput()); + output.WriteLine($"Day 17 - Part 1 - {result}"); + + Assert.Equal(expected, result); + } + + [Fact] + public void Part2_SampleInput_ProducesCorrectResponse() + { + var expected = 94; + + var result = solver.Part2(GetSampleInput()); + + Assert.Equal(expected, result); + } + + [Fact] + public void Part2_RealInput_ProducesCorrectResponse() + { + var expected = 1104; + + var result = solver.Part2(GetRealInput()); + output.WriteLine($"Day 17 - Part 2 - {result}"); + + Assert.Equal(expected, result); + } + } +}