diff --git a/12/HotSprings.Tests/ConditionTests.cs b/12/HotSprings.Tests/ConditionTests.cs new file mode 100644 index 0000000..afbbdf8 --- /dev/null +++ b/12/HotSprings.Tests/ConditionTests.cs @@ -0,0 +1,253 @@ +namespace HotSprings.Tests; + +public class ConditionTests +{ + [Theory] + [MemberData(nameof(TestData.ParseTestData), MemberType = typeof(TestData))] + public void Parse_WhenGivenConditionRecordInput_ItShouldReturnExpectedConditionInstance(string input, Condition expected) + { + var result = Condition.Parse(input); + result.Should().BeEquivalentTo(expected); + } + + [Theory] + [MemberData(nameof(TestData.GetNumberOfPossibleConfigurationsTestData), MemberType = typeof(TestData))] + public void GetNumberOfPossibleConfigurations_WhenGivenCondition_ItShouldReturnExpectedNumberOfPossibleConfigurations(Condition condition, long expected) + { + var result = condition.GetNumberOfPossibleConfigurations(); + result.Should().Be(expected); + } + + [Theory] + [MemberData(nameof(TestData.ParseAndUnFoldTestData), MemberType = typeof(TestData))] + public void ParseAndUnFold_WhenGivenConditionRecordInput_ItShouldReturnExpectedConditionInstance(string input, Condition expected) + { + var result = Condition.Parse(input, true); + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void Condition_WhenGivenInput_ItShouldReturnExpectedCount() + { + var input = File.ReadAllLines("INPUT.txt"); + var result = input + .Select(c => Condition.Parse(c)) + .Select(c => c.GetNumberOfPossibleConfigurations()) + .Sum(); + + result.Should().Be(6958); + } + + [Fact] + public void Condition_WhenGivenInputAndPartTwo_ItShouldReturnExpectedCount() + { + var input = File.ReadAllLines("INPUT.txt"); + var result = input + .Select(c => Condition.Parse(c, true)) + .Select(c => c.GetNumberOfPossibleConfigurations()) + .Sum(); + + result.Should().Be(6555315065024); + } + + public static class TestData + { + public static IEnumerable GetNumberOfPossibleConfigurationsTestData => + new List + { + new object[] + { + new Condition( + [1, 1, 3], + [7, 5, 3], + [ + SpringCondition.Unknown, + SpringCondition.Unknown, + SpringCondition.Unknown, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged + ] + ), + 1, + }, + new object[] + { + new Condition( + [1, 1, 3], + [7, 5, 3], + [ + SpringCondition.Operational, + SpringCondition.Unknown, + SpringCondition.Unknown, + SpringCondition.Operational, + SpringCondition.Operational, + SpringCondition.Unknown, + SpringCondition.Unknown, + SpringCondition.Operational, + SpringCondition.Operational, + SpringCondition.Operational, + SpringCondition.Unknown, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Operational, + ] + ), + 4, + }, + new object[] + { + new Condition( + [1, 3, 1, 6], + [11, 10, 6, 1], + [ + SpringCondition.Unknown, + SpringCondition.Damaged, + SpringCondition.Unknown, + SpringCondition.Damaged, + SpringCondition.Unknown, + SpringCondition.Damaged, + SpringCondition.Unknown, + SpringCondition.Damaged, + SpringCondition.Unknown, + SpringCondition.Damaged, + SpringCondition.Unknown, + SpringCondition.Damaged, + SpringCondition.Unknown, + SpringCondition.Damaged, + SpringCondition.Unknown, + ] + ), + 1, + }, + new object[] + { + new Condition( + [1, 6, 5], + [12, 6, 1], + [ + SpringCondition.Unknown, + SpringCondition.Unknown, + SpringCondition.Unknown, + SpringCondition.Unknown, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Operational, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged, + ] + ), + 4, + }, + new object[] + { + new Condition( + [3, 2, 1], + [6, 3, 1], + [ + SpringCondition.Unknown, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Unknown, + SpringCondition.Unknown, + SpringCondition.Unknown, + SpringCondition.Unknown, + SpringCondition.Unknown, + SpringCondition.Unknown, + SpringCondition.Unknown, + SpringCondition.Unknown, + ] + ), + 10, + } + }; + + public static IEnumerable ParseAndUnFoldTestData => + new List + { + new object[] + { + "#.#.### 1,1,3", + new Condition( + [1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 3], + [39, 37, 35, 31, 29, 27, 23, 21, 19, 15, 13, 11, 7, 5, 3], + [ + SpringCondition.Damaged, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Unknown, + SpringCondition.Damaged, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Unknown, + SpringCondition.Damaged, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Unknown, + SpringCondition.Damaged, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Unknown, + SpringCondition.Damaged, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged, + ] + ) + } + }; + + + public static IEnumerable ParseTestData => + new List + { + new object[] + { + "#.#.### 1,1,3", + new Condition( + [1, 1, 3], + [7, 5, 3], + [ + SpringCondition.Damaged, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Operational, + SpringCondition.Damaged, + SpringCondition.Damaged, + SpringCondition.Damaged + ] + ) + } + }; + } +} \ No newline at end of file diff --git a/12/HotSprings.Tests/HotSprings.Tests.csproj b/12/HotSprings.Tests/HotSprings.Tests.csproj index 05c2d07..5cb51e5 100644 --- a/12/HotSprings.Tests/HotSprings.Tests.csproj +++ b/12/HotSprings.Tests/HotSprings.Tests.csproj @@ -27,4 +27,10 @@ + + + PreserveNewest + + + diff --git a/12/HotSprings.Tests/UnitTest1.cs b/12/HotSprings.Tests/UnitTest1.cs deleted file mode 100644 index 60c68a0..0000000 --- a/12/HotSprings.Tests/UnitTest1.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace HotSprings.Tests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - - } -} \ No newline at end of file diff --git a/12/HotSprings/Program.cs b/12/HotSprings/Program.cs index 3751555..c41a588 100644 --- a/12/HotSprings/Program.cs +++ b/12/HotSprings/Program.cs @@ -1,2 +1,237 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using System.Diagnostics; + +namespace HotSprings; + +public class Program +{ + public static async Task Main(string[] args) + { + if (args.Length is 0) + { + Console.WriteLine("Please provide a path to the input file."); + return -1; + } + + if (File.Exists(args[0]) is false) + { + Console.WriteLine("The provided file does not exist."); + return -2; + } + + var isPart2 = args.Length > 1 && args[1] == "part2"; + var input = await File.ReadAllLinesAsync(args[0]); + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + var result = input + .Select(r => Condition.Parse(r, isPart2)) + .Sum(c => c.GetNumberOfPossibleConfigurations()); + + stopwatch.Stop(); + + Console.WriteLine($"The number of possible configurations is {result}. ({stopwatch.ElapsedMilliseconds}ms)"); + + return (int)result; + } +} + +/// +/// Represents a condition for a set of springs. +/// +/// The sizes of the contiguous groups of damaged springs. +/// The total number of springs required to be available for each contiguous group of damaged springs. +/// The conditions of the springs. +public class Condition( + List damagedContiguousGroupSizes, + List totalSpringsRequired, + List springs +) +{ + private const char OperationalSymbol = '.'; + private const char DamagedSymbol = '#'; + private const char UnknownSymbol = '?'; + + public List DamagedContiguousGroupSizes { get; set; } = damagedContiguousGroupSizes; + public List TotalSpringsRequired { get; set; } = totalSpringsRequired; + public List Springs { get; set; } = springs; + private readonly Dictionary<(int, int), long> _resultCache = []; + private bool IsNotOperational(int index) => Springs[index] is not SpringCondition.Operational; + private bool IsNotOperational(int index, int length) => Enumerable.Range(index, length).All(IsNotOperational); + + private bool IsDamaged(int index) => + index >= 0 && index < Springs.Count && Springs[index] is SpringCondition.Damaged; + + private bool RemainingSpringsAreNotDamaged(int index) => + Springs.Skip(index).All(sc => sc is not SpringCondition.Damaged); + + private long CalculateConfigurations(int springIndex, int groupIndex) + { + // check if all damaged groups have been checked + if (groupIndex == DamagedContiguousGroupSizes.Count) + { + // if none of remaining springs are damaged, then we have a valid configuration + return RemainingSpringsAreNotDamaged(springIndex) ? 1 : 0; + } + + // if the number of springs required for current group size is + // greater than the number of remaining springs, then we won't + // be able to achieve a valid configuration + if (springIndex + TotalSpringsRequired[groupIndex] > Springs.Count) + { + return 0; + } + + long sum = 0; + var groupSize = DamagedContiguousGroupSizes[groupIndex]; + var nextIndex = springIndex + groupSize + 1; + + // if the number of springs next to current spring + // equal to the group size are not operational + // and the spring next to the last spring in the group + // is not damaged, then we have a configuration that + // satisfies the current group size and can move + // on to the next group + if (IsNotOperational(springIndex, groupSize) && IsDamaged(springIndex + groupSize) is false) + { + sum += CountConfigurations(nextIndex, groupIndex + 1); + } + + // if the current spring is not damaged + // then we can move on to checking configuration + // with next spring but same group size. + if (IsDamaged(springIndex) is false) + { + sum += CountConfigurations(springIndex + 1, groupIndex); + } + + return sum; + } + + private long CountConfigurations(int springIndex, int groupIndex) + { + // use a tuple consisting of springIndex and groupIndex + // as a key for the cache + var key = (springIndex, groupIndex); + + // if the result for the current key is not in the cache + // then calculate it and add it to the cache + if (_resultCache.TryGetValue(key, out var result) is false) + { + result = CalculateConfigurations(springIndex, groupIndex); + _resultCache[key] = result; + } + + return result; + } + + /// + /// Returns the number of possible configurations for the condition. + /// + /// The number of possible configurations. + public long GetNumberOfPossibleConfigurations() => CountConfigurations(0, 0); + + /// + /// Parses the given input into a instance. + /// + /// The input to parse. + /// Whether to unfold the condition. + /// The parsed instance. + public static Condition Parse(string input, bool unfold = false) + { + var springs = new List(); + + var parts = input.Split( + ' ', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries + ); + var springsInput = parts[0]; + var contiguousGroupSizesInput = parts[1]; + + foreach (var spring in springsInput) + { + switch (spring) + { + case OperationalSymbol: + springs.Add(SpringCondition.Operational); + break; + case DamagedSymbol: + springs.Add(SpringCondition.Damaged); + break; + case UnknownSymbol: + springs.Add(SpringCondition.Unknown); + break; + default: + throw new Exception($"Invalid spring condition: {spring}"); + } + } + + var contiguousGroupSizes = contiguousGroupSizesInput + .Split( + ',', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries + ) + .Select(int.Parse) + .ToList(); + + if (unfold) + { + springs = [ + ..springs, + SpringCondition.Unknown, + ..springs, + SpringCondition.Unknown, + ..springs, + SpringCondition.Unknown, + ..springs, + SpringCondition.Unknown, + ..springs, + ]; + + contiguousGroupSizes = [ + ..contiguousGroupSizes, + ..contiguousGroupSizes, + ..contiguousGroupSizes, + ..contiguousGroupSizes, + ..contiguousGroupSizes, + ]; + } + + var springsRequired = 0; + var totalSpringsRequired = new int[contiguousGroupSizes.Count]; + + for (var i = contiguousGroupSizes.Count - 1; i >= 0; i--) + { + springsRequired += contiguousGroupSizes[i]; + totalSpringsRequired[i] = springsRequired; + springsRequired += 1; + } + + return new Condition( + contiguousGroupSizes, + [.. totalSpringsRequired], + springs + ); + } +} + +/// +/// Represents the condition of a spring. +/// +public enum SpringCondition +{ + /// + /// The spring is operational. + /// + Operational, + + /// + /// The spring is damaged. + /// + Damaged, + + /// + /// The condition of the spring is unknown. + /// + Unknown, +} \ No newline at end of file diff --git a/README.md b/README.md index 145f4e8..35d0522 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ dotnet build | 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/) | ⌛ | +| 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/) | ⌛ | | 14 | [Problem](./14/PROBLEM.md) | [Solution](./14/) | ⌛ | | 15 | [Problem](./15/PROBLEM.md) | [Solution](./15/) | ⌛ |