diff --git a/14/ParabolicReflectorDish.Tests/DishTests.cs b/14/ParabolicReflectorDish.Tests/DishTests.cs index db823bc..113e4f7 100644 --- a/14/ParabolicReflectorDish.Tests/DishTests.cs +++ b/14/ParabolicReflectorDish.Tests/DishTests.cs @@ -18,23 +18,147 @@ public class DishTests result.Should().Be(expected); } + [Theory] + [MemberData(nameof(TestData.CalculateTotalLoadWithSpinCycleData), MemberType = typeof(TestData))] + public void CalculateTotalLoad_WhenCalledWithSpinCycle_ItShouldReturnExpectedLoad(Dish dish, long expected) + { + var result = dish.CalculateTotalLoad(true); + result.Should().Be(expected); + } + + [Theory] + [MemberData(nameof(TestData.TiltTestData), MemberType = typeof(TestData))] + public void TiltDish_WhenCalled_ItShouldReturnExpectedDish(Dish dish, int times, Dish expected) + { + var result = dish.TiltDish(times); + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void CalculateTotalLoad_WhenCalledWithExample_ItShouldReturnExpectedLoad() + { + var input = File.ReadAllLines("EXAMPLE.txt"); + var dish = Dish.Parse(input); + var result = dish.CalculateTotalLoad(); + result.Should().Be(136); + } + + [Fact] + public void CalculateTotalLoad_WhenCalledWithInput_ItShouldReturnExpectedLoad() + { + var input = File.ReadAllLines("INPUT.txt"); + var dish = Dish.Parse(input); + var result = dish.CalculateTotalLoad(); + result.Should().Be(106997); + } + + [Fact] + public void CalculateTotalLoad_WhenCalledWithExampleAndSpinCycle_ItShouldReturnExpectedLoad() + { + var input = File.ReadAllLines("EXAMPLE.txt"); + var dish = Dish.Parse(input); + var result = dish.CalculateTotalLoad(true); + result.Should().Be(64); + } + + [Fact] + public void CalculateTotalLoad_WhenCalledWithInputAndSpinCycle_ItShouldReturnExpectedLoad() + { + var input = File.ReadAllLines("INPUT.txt"); + var dish = Dish.Parse(input); + var result = dish.CalculateTotalLoad(true); + result.Should().Be(99641); + } + public static class TestData { private static readonly Dish TestDish = new( [ - ['O', 'O', 'O', 'O', '.', '#', '.', 'O', '.', '.'], - ['O', 'O', '.', '.', '#', '.', '.', '.', '.', '#'], - ['O', 'O', '.', '.', 'O', '#', '#', '.', '.', 'O'], - ['O', '.', '.', '#', '.', 'O', 'O', '.', '.', '.'], - ['.', '.', '.', '.', '.', '.', '.', '.', '#', '.'], - ['.', '.', '#', '.', '.', '.', '.', '#', '.', '#'], - ['.', '.', 'O', '.', '.', '#', '.', 'O', '.', 'O'], - ['.', '.', 'O', '.', '.', '.', '.', '.', '.', '.'], + ['O', '.', '.', '.', '.', '#', '.', '.', '.', '.'], + ['O', '.', 'O', 'O', '#', '.', '.', '.', '.', '#'], + ['.', '.', '.', '.', '.', '#', '#', '.', '.', '.'], + ['O', 'O', '.', '#', 'O', '.', '.', '.', '.', 'O'], + ['.', 'O', '.', '.', '.', '.', '.', 'O', '#', '.'], + ['O', '.', '#', '.', '.', 'O', '.', '#', '.', '#'], + ['.', '.', 'O', '.', '.', '#', 'O', '.', '.', 'O'], + ['.', '.', '.', '.', '.', '.', '.', 'O', '.', '.'], ['#', '.', '.', '.', '.', '#', '#', '#', '.', '.'], - ['#', '.', '.', '.', '.', '#', '.', '.', '.', '.'], + ['#', 'O', 'O', '.', '.', '#', '.', '.', '.', '.'], ] ); + public static IEnumerable TiltTestData => + new List + { + new object[] + { + TestDish, + 1, + new Dish( + [ + ['.', '.', '.', '.', '.', '#', '.', '.', '.', '.'], + ['.', '.', '.', '.', '#', '.', '.', '.', 'O', '#'], + ['.', '.', '.', 'O', 'O', '#', '#', '.', '.', '.'], + ['.', 'O', 'O', '#', '.', '.', '.', '.', '.', '.'], + ['.', '.', '.', '.', '.', 'O', 'O', 'O', '#', '.'], + ['.', 'O', '#', '.', '.', '.', 'O', '#', '.', '#'], + ['.', '.', '.', '.', 'O', '#', '.', '.', '.', '.'], + ['.', '.', '.', '.', '.', '.', 'O', 'O', 'O', 'O'], + ['#', '.', '.', '.', 'O', '#', '#', '#', '.', '.'], + ['#', '.', '.', 'O', 'O', '#', '.', '.', '.', '.'], + ] + ) + }, + new object[] + { + TestDish, + 2, + new Dish( + [ + ['.', '.', '.', '.', '.', '#', '.', '.', '.', '.'], + ['.', '.', '.', '.', '#', '.', '.', '.', 'O', '#'], + ['.', '.', '.', '.', '.', '#', '#', '.', '.', '.'], + ['.', '.', 'O', '#', '.', '.', '.', '.', '.', '.'], + ['.', '.', '.', '.', '.', 'O', 'O', 'O', '#', '.'], + ['.', 'O', '#', '.', '.', '.', 'O', '#', '.', '#'], + ['.', '.', '.', '.', 'O', '#', '.', '.', '.', 'O'], + ['.', '.', '.', '.', '.', '.', '.', 'O', 'O', 'O'], + ['#', '.', '.', 'O', 'O', '#', '#', '#', '.', '.'], + ['#', '.', 'O', 'O', 'O', '#', '.', '.', '.', 'O'], + ] + ), + }, + new object[] + { + TestDish, + 3, + new Dish( + [ + ['.', '.', '.', '.', '.', '#', '.', '.', '.', '.'], + ['.', '.', '.', '.', '#', '.', '.', '.', 'O', '#'], + ['.', '.', '.', '.', '.', '#', '#', '.', '.', '.'], + ['.', '.', 'O', '#', '.', '.', '.', '.', '.', '.'], + ['.', '.', '.', '.', '.', 'O', 'O', 'O', '#', '.'], + ['.', 'O', '#', '.', '.', '.', 'O', '#', '.', '#'], + ['.', '.', '.', '.', 'O', '#', '.', '.', '.', 'O'], + ['.', '.', '.', '.', '.', '.', '.', 'O', 'O', 'O'], + ['#', '.', '.', '.', 'O', '#', '#', '#', '.', 'O'], + ['#', '.', 'O', 'O', 'O', '#', '.', '.', '.', 'O'], + ] + ), + } + }; + + public static IEnumerable CalculateTotalLoadWithSpinCycleData => + new List + { + new object[] + { + TestDish, + 64, + } + }; + public static IEnumerable CalculateTotalLoadTestData => new List { diff --git a/14/ParabolicReflectorDish.Tests/ParabolicReflectorDish.Tests.csproj b/14/ParabolicReflectorDish.Tests/ParabolicReflectorDish.Tests.csproj index 44829c1..556ee78 100644 --- a/14/ParabolicReflectorDish.Tests/ParabolicReflectorDish.Tests.csproj +++ b/14/ParabolicReflectorDish.Tests/ParabolicReflectorDish.Tests.csproj @@ -27,4 +27,16 @@ + + + PreserveNewest + + + + + + PreserveNewest + + + diff --git a/14/ParabolicReflectorDish/Program.cs b/14/ParabolicReflectorDish/Program.cs index da67d77..046e31b 100644 --- a/14/ParabolicReflectorDish/Program.cs +++ b/14/ParabolicReflectorDish/Program.cs @@ -18,6 +18,7 @@ public class Program return -2; } + var isPart2 = args.Length > 1 && args[1] == "part2"; var input = await File.ReadAllLinesAsync(args[0]); var stopwatch = new Stopwatch(); @@ -25,7 +26,7 @@ public class Program var result = Dish .Parse(input) - .CalculateTotalLoad(); + .CalculateTotalLoad(isPart2); stopwatch.Stop(); @@ -35,6 +36,11 @@ public class Program } } +/// +/// Represents a parabolic reflector dish. +/// +/// The rows of the dish. +/// An instance of the class. public class Dish( List> rows ) @@ -43,12 +49,18 @@ public class Dish( private const char SquareRockSymbol = '#'; private const char EmptySpaceSymbol = '.'; + /// + /// Gets the rows of the dish. + /// public List> Rows { get; init; } = rows; private List> TiltDishToNorth(List> rows) { // create a copy of the rows which we can modify - var modifiedRows = rows.ToList(); + var modifiedRows = rows + .Select(r => r.ToList()) + .ToList(); + var numOfColumns = modifiedRows[0].Count; var numOfRows = modifiedRows.Count; @@ -97,9 +109,233 @@ public class Dish( return modifiedRows; } - public long CalculateTotalLoad() + private List> TiltDishToWest(List> rows) { - var rows = TiltDishToNorth(Rows); + // create a copy of the rows which we can modify + var modifiedRows = rows + .Select(r => r.ToList()) + .ToList(); + + var numOfRows = modifiedRows.Count; + var numOfColumns = modifiedRows[0].Count; + + // iterate over the rows from top to bottom + for (var currentRowIndex = 0; currentRowIndex < numOfRows; currentRowIndex++) + { + // iterate over the columns from left to right + for (var currentColumnIndex = 0; currentColumnIndex < numOfRows; currentColumnIndex++) + { + // get the current symbol + var current = modifiedRows[currentRowIndex][currentColumnIndex]; + + // if the current symbol is an empty space + if (current is EmptySpaceSymbol) + { + // iterate over the columns to the left of the current column + for (var i = currentColumnIndex + 1; i < numOfColumns; i++) + { + // get the next symbol in the current row + var next = modifiedRows[currentRowIndex][i]; + + // if the next symbol is a square rock + // we can stop iterating over the columns to the left + // because the square rock blocks moving + // any round rock to fill the empty space + if (next is SquareRockSymbol) + { + break; + } + + // if the next symbol is a round rock + // we can move the round rock to the empty space + // and stop iterating over the columns to the left + if (next is RoundRockSymbol) + { + modifiedRows[currentRowIndex][currentColumnIndex] = RoundRockSymbol; + modifiedRows[currentRowIndex][i] = EmptySpaceSymbol; + break; + } + } + } + } + } + + return modifiedRows; + } + + private List> TiltDishToSouth(List> rows) + { + // create a copy of the rows which we can modify + var modifiedRows = rows + .Select(r => r.ToList()) + .ToList(); + + var numOfColumns = modifiedRows[0].Count; + var numOfRows = modifiedRows.Count; + + // iterate over the columns from left to right + for (var currentColumnIndex = 0; currentColumnIndex < numOfColumns; currentColumnIndex++) + { + // iterate over the rows from bottom to top + for (var currentRowIndex = numOfRows - 1; currentRowIndex >= 0; currentRowIndex--) + { + // get the current symbol + var current = modifiedRows[currentRowIndex][currentColumnIndex]; + + // if the current symbol is an empty space + if (current is EmptySpaceSymbol) + { + // iterate over the rows above the current row + // starting from the row before the current row + for (var i = currentRowIndex - 1; i >= 0; i--) + { + // get the next symbol in the current column + var next = modifiedRows[i][currentColumnIndex]; + + // if the next symbol is a square rock + // we can stop iterating over the rows below + // because the square rock blocks moving + // any round rock to fill the empty space + if (next is SquareRockSymbol) + { + break; + } + + // if the next symbol is a round rock + // we can move the round rock to the empty space + // and stop iterating over the rows below + if (next is RoundRockSymbol) + { + modifiedRows[currentRowIndex][currentColumnIndex] = RoundRockSymbol; + modifiedRows[i][currentColumnIndex] = EmptySpaceSymbol; + break; + } + } + } + } + } + + return modifiedRows; + } + + private List> TiltDishToEast(List> rows) + { + // create a copy of the rows which we can modify + var modifiedRows = rows + .Select(r => r.ToList()) + .ToList(); + + var numOfRows = modifiedRows.Count; + var numOfColumns = modifiedRows[0].Count; + + // iterate over the rows from top to bottom + for (var currentRowIndex = 0; currentRowIndex < numOfRows; currentRowIndex++) + { + // iterate over the columns from right to left + for (var currentColumnIndex = numOfColumns - 1; currentColumnIndex >= 0; currentColumnIndex--) + { + // get the current symbol + var current = modifiedRows[currentRowIndex][currentColumnIndex]; + + // if the current symbol is an empty space + if (current is EmptySpaceSymbol) + { + // iterate over the columns to the left of the current column + for (var i = currentColumnIndex - 1; i >= 0; i--) + { + // get the next symbol in the current row + var next = modifiedRows[currentRowIndex][i]; + + // if the next symbol is a square rock + // we can stop iterating over the columns to the left + // because the square rock blocks moving + // any round rock to fill the empty space + if (next is SquareRockSymbol) + { + break; + } + + // if the next symbol is a round rock + // we can move the round rock to the empty space + // and stop iterating over the columns to the left + if (next is RoundRockSymbol) + { + modifiedRows[currentRowIndex][currentColumnIndex] = RoundRockSymbol; + modifiedRows[currentRowIndex][i] = EmptySpaceSymbol; + break; + } + } + } + } + } + + return modifiedRows; + } + + /// + /// Tilt the dish to the north, west, south and east. + /// + /// The number of cycles to tilt the dish. + /// An instance of the class. + public Dish TiltDish(long numOfCycles) + { + var modifiedRows = Rows + .Select(r => r.ToList()) + .ToList(); + + // create a cache to store the modified rows + // and the index of the cycle in which they were modified + // this should allow us to detect cycles + var cache = new Dictionary(); + + for (var i = 0; i < numOfCycles; i++) + { + modifiedRows = TiltDishToNorth(modifiedRows); + modifiedRows = TiltDishToWest(modifiedRows); + modifiedRows = TiltDishToSouth(modifiedRows); + modifiedRows = TiltDishToEast(modifiedRows); + + // use the modified rows as a key for the cache + var key = string.Join(Environment.NewLine, modifiedRows.Select(r => string.Join(string.Empty, r))); + + // if the cache already contains the key + if (cache.TryGetValue(key, out var matchingIndex)) + { + // calculate the start index of the current cycle + // and identify the key for the start index value + var cycleLength = i - matchingIndex; + var remainingCycles = numOfCycles - i; + var cycleIndex = remainingCycles % cycleLength; + var cycleStartIndex = cycleIndex + matchingIndex - 1; + var startCycleKey = cache.First(c => c.Value == cycleStartIndex).Key; + + // parse the key for the start index value + // and use it as the modified rows + modifiedRows = startCycleKey + .Split(Environment.NewLine) + .Select(s => s.ToList()) + .ToList(); + break; + } + + // add the modified rows to the cache + cache.Add(key, i); + } + + return new Dish(modifiedRows); + } + + /// + /// Calculate the total load of the dish. + /// + /// Whether to spin the dish. + /// The total load of the dish. + public long CalculateTotalLoad(bool spinDish = false) + { + var rows = spinDish + ? TiltDish(1_000_000_000).Rows + : TiltDishToNorth(Rows); + rows.Reverse(); return rows @@ -111,6 +347,11 @@ public class Dish( .Sum(); } + /// + /// Parse the input into an instance of the class. + /// + /// The input to parse. + /// An instance of the class. public static Dish Parse(string[] input) { var inputList = input diff --git a/README.md b/README.md index eb8f2d6..1e6cc7b 100644 --- a/README.md +++ b/README.md @@ -42,30 +42,30 @@ dotnet build ## Challenges -| Day | Problem | Solution | Status | Notes | -| --- | -------------------------- | :---------------------------------: | :----: | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| 01 | [Problem](./01/PROBLEM.md) | [Solution](./01/Trebuchet/) | ✅ | The trickiest part here was accounting for overlapping digit words. | -| 02 | [Problem](./02/PROBLEM.md) | [Solution](./02/CubeConundrum/) | ✅ | The key to me here was to parse the input into a useful model. | -| 03 | [Problem](./03/PROBLEM.md) | [Solution](./03/GearRatios/) | ✅ | The edge case that got me here was lines ending with a part number. | -| 04 | [Problem](./04/PROBLEM.md) | [Solution](./04/Scratchcards/) | ✅ | Part 2 gets out of hand quickly with just 200 cards. | -| 05 | [Problem](./05/PROBLEM.md) | [Solution](./05/IYGASAF/) | ✅ | I brute forced part 2 using parallelism. I know shame. | -| 06 | [Problem](./06/PROBLEM.md) | [Solution](./06/WaitForIt/) | ✅ | Thank goodness part 2 was not like 5's part 2. 😅 | -| 07 | [Problem](./07/PROBLEM.md) | [Solution](./07/CamelCards/) | ✅ | What took me longest here was I missed a case when jokers are wild and there are three groups of cards. | -| 08 | [Problem](./08/PROBLEM.md) | [Solution](./08/HauntedWasteland/) | ✅ | Got to implement a least common multiple algorithm for part 2. | -| 09 | [Problem](./09/PROBLEM.md) | [Solution](./09/MirageMaintenance/) | ✅ | Part 1 took me unnecessarily long. The bug was summing a line to check for all zeros is bad idea when negative numbers are involved. | -| 10 | [Problem](./10/PROBLEM.md) | [Solution](./10/PipeMaze/) | ✅ | So...my solutions are rather slow, but they work. Part 2 appears to have a mathematical solution, but scanning is what I came up with on my own. | -| 11 | [Problem](./11/PROBLEM.md) | [Solution](./11/CosmicExpansion/) | ✅ | Expanding the universe was fine in part 1, but part 2 showed I actually needed to calculate the expansion instead of expanding the input. | -| 12 | [Problem](./12/PROBLEM.md) | [Solution](./12/HotSprings/) | ✅ | This one stretched my skills. I needed lots of help from the interwebz. | -| 13 | [Problem](./13/PROBLEM.md) | [Solution](./13/PointOfIncidence/) | ✅ | This solution is the one I think I'm most proud of so far. | -| 14 | [Problem](./14/PROBLEM.md) | [Solution](./14/) | ⌛ | -| 15 | [Problem](./15/PROBLEM.md) | [Solution](./15/) | ⌛ | -| 16 | [Problem](./16/PROBLEM.md) | [Solution](./16/) | ⌛ | -| 17 | [Problem](./17/PROBLEM.md) | [Solution](./17/) | ⌛ | -| 18 | [Problem](./18/PROBLEM.md) | [Solution](./18/) | ⌛ | -| 19 | [Problem](./19/PROBLEM.md) | [Solution](./19/) | ⌛ | -| 20 | [Problem](./20/PROBLEM.md) | [Solution](./20/) | ⌛ | -| 21 | [Problem](./21/PROBLEM.md) | [Solution](./21/) | ⌛ | -| 22 | [Problem](./22/PROBLEM.md) | [Solution](./22/) | ⌛ | -| 23 | [Problem](./23/PROBLEM.md) | [Solution](./23/) | ⌛ | -| 24 | [Problem](./24/PROBLEM.md) | [Solution](./24/) | ⌛ | -| 25 | [Problem](./25/PROBLEM.md) | [Solution](./25/) | ⌛ | +| Day | Problem | Solution | Status | Notes | +| --- | -------------------------- | :--------------------------------------: | :----: | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| 01 | [Problem](./01/PROBLEM.md) | [Solution](./01/Trebuchet/) | ✅ | The trickiest part here was accounting for overlapping digit words. | +| 02 | [Problem](./02/PROBLEM.md) | [Solution](./02/CubeConundrum/) | ✅ | The key to me here was to parse the input into a useful model. | +| 03 | [Problem](./03/PROBLEM.md) | [Solution](./03/GearRatios/) | ✅ | The edge case that got me here was lines ending with a part number. | +| 04 | [Problem](./04/PROBLEM.md) | [Solution](./04/Scratchcards/) | ✅ | Part 2 gets out of hand quickly with just 200 cards. | +| 05 | [Problem](./05/PROBLEM.md) | [Solution](./05/IYGASAF/) | ✅ | I brute forced part 2 using parallelism. I know shame. | +| 06 | [Problem](./06/PROBLEM.md) | [Solution](./06/WaitForIt/) | ✅ | Thank goodness part 2 was not like 5's part 2. 😅 | +| 07 | [Problem](./07/PROBLEM.md) | [Solution](./07/CamelCards/) | ✅ | What took me longest here was I missed a case when jokers are wild and there are three groups of cards. | +| 08 | [Problem](./08/PROBLEM.md) | [Solution](./08/HauntedWasteland/) | ✅ | Got to implement a least common multiple algorithm for part 2. | +| 09 | [Problem](./09/PROBLEM.md) | [Solution](./09/MirageMaintenance/) | ✅ | Part 1 took me unnecessarily long. The bug was summing a line to check for all zeros is bad idea when negative numbers are involved. | +| 10 | [Problem](./10/PROBLEM.md) | [Solution](./10/PipeMaze/) | ✅ | So...my solutions are rather slow, but they work. Part 2 appears to have a mathematical solution, but scanning is what I came up with on my own. | +| 11 | [Problem](./11/PROBLEM.md) | [Solution](./11/CosmicExpansion/) | ✅ | Expanding the universe was fine in part 1, but part 2 showed I actually needed to calculate the expansion instead of expanding the input. | +| 12 | [Problem](./12/PROBLEM.md) | [Solution](./12/HotSprings/) | ✅ | This one stretched my skills. I needed lots of help from the interwebz. | +| 13 | [Problem](./13/PROBLEM.md) | [Solution](./13/PointOfIncidence/) | ✅ | This solution is the one I think I'm most proud of so far. | +| 14 | [Problem](./14/PROBLEM.md) | [Solution](./14/ParabolicReflectorDish/) | ✅ | Cycles and a cache...that's all I'm going to say. | +| 15 | [Problem](./15/PROBLEM.md) | [Solution](./15/) | ⌛ | +| 16 | [Problem](./16/PROBLEM.md) | [Solution](./16/) | ⌛ | +| 17 | [Problem](./17/PROBLEM.md) | [Solution](./17/) | ⌛ | +| 18 | [Problem](./18/PROBLEM.md) | [Solution](./18/) | ⌛ | +| 19 | [Problem](./19/PROBLEM.md) | [Solution](./19/) | ⌛ | +| 20 | [Problem](./20/PROBLEM.md) | [Solution](./20/) | ⌛ | +| 21 | [Problem](./21/PROBLEM.md) | [Solution](./21/) | ⌛ | +| 22 | [Problem](./22/PROBLEM.md) | [Solution](./22/) | ⌛ | +| 23 | [Problem](./23/PROBLEM.md) | [Solution](./23/) | ⌛ | +| 24 | [Problem](./24/PROBLEM.md) | [Solution](./24/) | ⌛ | +| 25 | [Problem](./25/PROBLEM.md) | [Solution](./25/) | ⌛ |