From 2ffa28217ede0babee09e80303c7d2626b0b5db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Piotr=20Moru=C5=9B?= Date: Thu, 18 Jul 2024 14:42:17 +0200 Subject: [PATCH] Finished part 1 of the puzzle --- Day12 - Hot Springs/Day12Solver.cs | 7 +- Day12 - Hot Springs/Day12SolverOptions.cs | 3 - Day12 - Hot Springs/InputReader.cs | 65 ++----- .../SpringArrangementCounter.cs | 162 ++++++++++++++++++ Day12 - Hot Springs/SpringArrangementMemo.cs | 31 ++++ Day12 - Hot Springs/SpringCondition.cs | 8 + Day12 - Hot Springs/SpringRow.cs | 7 + Tests/Day12Tests.cs | 2 +- 8 files changed, 230 insertions(+), 55 deletions(-) create mode 100644 Day12 - Hot Springs/SpringArrangementCounter.cs create mode 100644 Day12 - Hot Springs/SpringArrangementMemo.cs create mode 100644 Day12 - Hot Springs/SpringCondition.cs create mode 100644 Day12 - Hot Springs/SpringRow.cs diff --git a/Day12 - Hot Springs/Day12Solver.cs b/Day12 - Hot Springs/Day12Solver.cs index 9d16a09..029e216 100644 --- a/Day12 - Hot Springs/Day12Solver.cs +++ b/Day12 - Hot Springs/Day12Solver.cs @@ -8,11 +8,11 @@ public sealed class Day12Solver : DaySolver public override int Day => 12; public override string Title => "Hot Springs"; - private readonly IReadOnlyList _rows; + private readonly IReadOnlyList _rows; public Day12Solver(Day12SolverOptions options) : base(options) { - var inputReader = new InputReader(options); + var inputReader = new InputReader(); _rows = inputReader.ReadInput(InputLines); } @@ -27,7 +27,8 @@ public Day12Solver() : this(new Day12SolverOptions()) public override string SolvePart1() { - var result = _rows.Select(r => r.Springs.Count(s => s == SpringCondition.Unknown)).Max(); + var counter = new SpringArrangementCounter(); + var result = _rows.Select(r => counter.CountPossibleArrangements(r)).Sum(); return result.ToString(); } diff --git a/Day12 - Hot Springs/Day12SolverOptions.cs b/Day12 - Hot Springs/Day12SolverOptions.cs index afd10a4..7da86b9 100644 --- a/Day12 - Hot Springs/Day12SolverOptions.cs +++ b/Day12 - Hot Springs/Day12SolverOptions.cs @@ -4,7 +4,4 @@ namespace AdventOfCode.Year2023.Day12; public sealed class Day12SolverOptions : DaySolverOptions { - public char OperationalSpringChar { get; set; } = '.'; - public char DamagedSpringChar { get; set; } = '#'; - public char UnknownSpringChar { get; set; } = '?'; } diff --git a/Day12 - Hot Springs/InputReader.cs b/Day12 - Hot Springs/InputReader.cs index 9472366..43bc999 100644 --- a/Day12 - Hot Springs/InputReader.cs +++ b/Day12 - Hot Springs/InputReader.cs @@ -5,22 +5,15 @@ namespace AdventOfCode.Year2023.Day12; internal sealed class InputReader { - private readonly char _operationalSpringChar; - private readonly char _damagedSpringChar; - private readonly char _unknownSpringChar; + private const char OperationalSpringChar = '.'; + private const char DamagedSpringChar = '#'; + private const char UnknownSpringChar = '?'; private static readonly Regex SpringRowRegex = new(@"^\s*(.+)\s+([\d,]+)\s*$", RegexOptions.Compiled); - public InputReader(Day12SolverOptions options) + public SpringRow[] ReadInput(IEnumerable inputLines) { - _operationalSpringChar = options.OperationalSpringChar; - _damagedSpringChar = options.DamagedSpringChar; - _unknownSpringChar = options.UnknownSpringChar; - } - - public IReadOnlyList ReadInput(IEnumerable inputLines) - { - List rows = new(1000); + List rows = new(1000); foreach (string line in inputLines) { if (string.IsNullOrWhiteSpace(line)) @@ -40,13 +33,13 @@ public IReadOnlyList ReadInput(IEnumerable inputLines) var springGroupSizesSpan = match.Groups[2].ValueSpan; var springGroupSizes = ReadSpringGroupSizes(springGroupSizesSpan); - rows.Add(new InputRow(springs, springGroupSizes)); + rows.Add(new SpringRow(springs, springGroupSizes)); } - return rows; + return rows.ToArray(); } - private IReadOnlyList ReadSprings(ReadOnlySpan springs) + private SpringCondition[] ReadSprings(ReadOnlySpan springs) { springs = springs.Trim(); var result = new SpringCondition[springs.Length]; @@ -58,27 +51,16 @@ private IReadOnlyList ReadSprings(ReadOnlySpan springs) return result; } - private SpringCondition ParseSpringType(char c) - { - if (c == _operationalSpringChar) - { - return SpringCondition.Operational; - } - - if (c == _damagedSpringChar) - { - return SpringCondition.Damaged; - } - - if (c == _unknownSpringChar) + private static SpringCondition ParseSpringType(char c) + => c switch { - return SpringCondition.Unknown; - } - - throw new InputException($"Invalid spring type character ('{c}')"); - } + OperationalSpringChar => SpringCondition.Operational, + DamagedSpringChar => SpringCondition.Damaged, + UnknownSpringChar => SpringCondition.Unknown, + _ => throw new InputException($"Invalid spring type character ('{c}')") + }; - private IReadOnlyList ReadSpringGroupSizes(ReadOnlySpan springGroupSizes) + private int[] ReadSpringGroupSizes(ReadOnlySpan springGroupSizes) { int count = springGroupSizes.Count(','); var result = new List(count + 1); @@ -91,19 +73,6 @@ private IReadOnlyList ReadSpringGroupSizes(ReadOnlySpan springGroupSi result.Add(size); } - return result; + return result.ToArray(); } } - -internal enum SpringCondition -{ - Unknown, - Operational, - Damaged, -} - -internal class InputRow(IReadOnlyList springs, IReadOnlyList springGroupSizes) -{ - public IReadOnlyList Springs { get; } = springs; - public IReadOnlyList SpringGroupSizes { get; } = springGroupSizes; -} diff --git a/Day12 - Hot Springs/SpringArrangementCounter.cs b/Day12 - Hot Springs/SpringArrangementCounter.cs new file mode 100644 index 0000000..b6a83a2 --- /dev/null +++ b/Day12 - Hot Springs/SpringArrangementCounter.cs @@ -0,0 +1,162 @@ +namespace AdventOfCode.Year2023.Day12; + +internal sealed class SpringArrangementCounter +{ + private readonly SpringArrangementMemo _memo = new(); + + public int CountPossibleArrangements(SpringRow row) + { + // Extend row with one operational spring at the end, with that making sure that the last spring cannot + // be part of a damaged group, and so we can add a group size of size 0 at the end also + row = new( + [..row.ConditionRecords, SpringCondition.Operational], + [..row.DamagedGroupSizes, 0] + ); + + _memo.ClearAndResizeFor(row); + + for (int springIndex = 0; springIndex < row.ConditionRecords.Length; springIndex++) + { + // Spring index represents the index in condition records of the spring currently being considered + for (int groupIndex = 0; groupIndex < row.DamagedGroupSizes.Length; groupIndex++) + { + // Group index represents the index of damage group currently being considered + for (int groupSize = 0; groupSize <= row.DamagedGroupSizes[groupIndex]; groupSize++) + { + // Group size represents the current size of the damage group currently being considered (it may have + // just started with current group size of 0 all the way to just ended with the full group size) + _memo[springIndex, groupIndex, groupSize] = CalculatePossibleArrangementsAt(springIndex, groupIndex, groupSize, row); + // We can safely use CalculatePossibleArrangementsAt as we are at either springIndex = 0, + // or all values for previous spring springIndex - 1 are already calculated and stored in memo + // since we use that as the outer loop variable. + } + } + } + + // Final answer is stored for the last spring (index = row.ConditionRecords.Length - 1), + // last group (index = row.DamagedGroupSizes.Length - 1) + // and with the last group size of 0 - we started the function with adding that 0 size group at the end. + return _memo[row.ConditionRecords.Length - 1, row.DamagedGroupSizes.Length - 1, 0]; + } + + /// + /// Calculated number of possible arrangements at the given spring index, group index and group size provided that + /// all possible arrangements for previous spring (at currentSpringIndex - 1) are already stored in the memo (base + /// cases for currentSpringIndex = 0 are handled correctly as well). + /// + private int CalculatePossibleArrangementsAt(int currentSpringIndex, int currentGroupIndex, int currentGroupSize, SpringRow row) + { + var springCondition = row.ConditionRecords[currentSpringIndex]; + + if (currentSpringIndex == 0) + { + // Base cases for when we are considering the first spring at index 0 + return CalculateBasePossibleArrangements(currentGroupIndex, currentGroupSize, springCondition); + } + + int possibleArrangementCount = 0; + + if (springCondition is SpringCondition.Operational or SpringCondition.Unknown) + { + possibleArrangementCount += CalculatePossibleArrangementsAssumingCurrentSpringOperational(currentSpringIndex, currentGroupIndex, currentGroupSize, row); + } + + if (springCondition is SpringCondition.Damaged or SpringCondition.Unknown) + { + possibleArrangementCount += CalculatePossibleArrangementsAssumingCurrentSpringDamaged(currentSpringIndex, currentGroupIndex, currentGroupSize); + } + + return possibleArrangementCount; + } + + /// + /// Calculates values for base cases of first spring at index 0, does not use memo as it is not needed. + /// + private static int CalculateBasePossibleArrangements(int currentGroupIndex, int currentGroupSize, SpringCondition firstSpringCondition) + { + // Base arrangements work on an assumption of i = 0 as base only applies to the first spring + + if (currentGroupIndex > 0) + { + // This is not possible as we can't consider group any other that first one if we are on a first character + return 0; + } + + // Based on the first spring condition and size of last group, we can have only either one possible arrangement (1) + // or given arrangement is impossible (0) + + return firstSpringCondition switch + { + // The first spring being damaged is only possible if the last group size is exactly 1 (this spring), otherwise it is not possible + SpringCondition.Damaged => currentGroupSize == 1 ? 1 : 0, + // The first spring being operational is only possible if the last group size is exactly 0 (no damaged spring), otherwise it is not possible + SpringCondition.Operational => currentGroupSize == 0 ? 1 : 0, + // If the first spring is unknown, `k` can be either 0 or 1, since we don't know the state of the first spring, but can't have a group size > 1 as we have only considered single spring + SpringCondition.Unknown => currentGroupSize < 2 ? 1 : 0, + _ => throw new DaySolverException($"Invalid spring condition {firstSpringCondition}") + }; + } + + /// + /// Calculates number of possible arrangements assuming that the current spring (at currentSpringIndex) is operational. + /// This can either be when the state of the spring is known to be operational, or it is unknown and *can* be operational. + /// It may use memo from previous spring (at index currentSpringIndex - 1) to calculate the number of possible arrangements. + /// + private int CalculatePossibleArrangementsAssumingCurrentSpringOperational(int currentSpringIndex, int currentGroupIndex, int currentGroupSize, SpringRow row) + { + if (currentGroupSize > 0) + { + // This is not possible since we cannot have a current group size of more than 0 while considering the last spring as operational. + return 0; + } + + if (currentGroupIndex == 0) + { + // If we are considering first damage group with empty final group (currentGroupSize == 0 asserted by previous condition) + // that means there should be one arrangement possible with no damaged springs before the current spring - otherwise that is not possible. + bool containsDamagedSprings = ContainsDamaged(row.ConditionRecords.AsSpan()[..currentSpringIndex]); + return containsDamagedSprings ? 0 : 1; + } + + // Getting to this arrangement from previous spring (currentSpringIndex - 1) consists of two scenarios, either considering + // previous damage group (currentGroupIndex - 1) ending with its full group size (row.DamagedGroupSizes[j - 1]) or considering + // current damage group (currentGroupIndex) ending with empty group size (0) + return _memo[currentSpringIndex - 1, currentGroupIndex - 1, row.DamagedGroupSizes[currentGroupIndex - 1]] + + _memo[currentSpringIndex - 1, currentGroupIndex, 0]; + } + + /// + /// Calculates number of possible arrangements assuming that the current spring (at currentSpringIndex) is damaged. + /// This can either be when the state of the spring is known to be damaged, or it is unknown and *can* be damaged. + /// It may use memo from previous spring (at index currentSpringIndex - 1) to calculate the number of possible arrangements. + /// + private int CalculatePossibleArrangementsAssumingCurrentSpringDamaged(int currentSpringIndex, int currentGroupIndex, int currentGroupSize) + { + if (currentGroupSize == 0) + { + // This is not possible since we cannot have a current group size of 0 while considering the current spring as damaged. + return 0; + } + + // Number of ways to get to this arrangement is equal as the number of ways to get to the previous spring (currentSpringIndex - 1) + // with the same damage group (currentGroupIndex), but one less size of the current group size (currentGroupIndex - 1) + // as this step is just adding one more damaged spring to the group. + return _memo[currentSpringIndex - 1, currentGroupIndex, currentGroupSize - 1]; + } + + /// + /// Returns whether contains any damaged springs. + /// + private static bool ContainsDamaged(ReadOnlySpan springSpan) + { + foreach (var item in springSpan) + { + if (item is SpringCondition.Damaged) + { + return true; + } + } + + return false; + } +} diff --git a/Day12 - Hot Springs/SpringArrangementMemo.cs b/Day12 - Hot Springs/SpringArrangementMemo.cs new file mode 100644 index 0000000..e0141b7 --- /dev/null +++ b/Day12 - Hot Springs/SpringArrangementMemo.cs @@ -0,0 +1,31 @@ +namespace AdventOfCode.Year2023.Day12; + +internal sealed class SpringArrangementMemo +{ + private int[,,] _memo = new int[0, 0, 0]; + + public void ClearAndResizeFor(SpringRow row) + { + int nofSprings = row.ConditionRecords.Length; + int nofGroups = row.DamagedGroupSizes.Length; + int maxGroupSize = row.DamagedGroupSizes.Max(); + + if (_memo.GetLength(0) < nofSprings || _memo.GetLength(1) < nofGroups || _memo.GetLength(2) <= maxGroupSize) + { + int newSpringsDimension = Math.Max(_memo.GetLength(0), nofSprings); + int newGroupsDimension = Math.Max(_memo.GetLength(1), nofGroups); + int newGroupSizeDimension = Math.Max(_memo.GetLength(2), maxGroupSize) + 1; + _memo = new int[newSpringsDimension, newGroupsDimension, newGroupSizeDimension]; + } + else + { + Array.Clear(_memo); + } + } + + public int this[int springIndex, int groupIndex, int groupSize] + { + get => _memo[springIndex, groupIndex, groupSize]; + set => _memo[springIndex, groupIndex, groupSize] = value; + } +} diff --git a/Day12 - Hot Springs/SpringCondition.cs b/Day12 - Hot Springs/SpringCondition.cs new file mode 100644 index 0000000..cd6e3b2 --- /dev/null +++ b/Day12 - Hot Springs/SpringCondition.cs @@ -0,0 +1,8 @@ +namespace AdventOfCode.Year2023.Day12; + +internal enum SpringCondition +{ + Unknown, + Operational, + Damaged, +} diff --git a/Day12 - Hot Springs/SpringRow.cs b/Day12 - Hot Springs/SpringRow.cs new file mode 100644 index 0000000..c1df563 --- /dev/null +++ b/Day12 - Hot Springs/SpringRow.cs @@ -0,0 +1,7 @@ +namespace AdventOfCode.Year2023.Day12; + +internal class SpringRow(SpringCondition[] conditionRecords, int[] damagedGroupSizes) +{ + public SpringCondition[] ConditionRecords { get; } = conditionRecords; + public int[] DamagedGroupSizes { get; } = damagedGroupSizes; +} diff --git a/Tests/Day12Tests.cs b/Tests/Day12Tests.cs index 3480501..c68adf6 100644 --- a/Tests/Day12Tests.cs +++ b/Tests/Day12Tests.cs @@ -12,7 +12,7 @@ public sealed class Day12Tests : BaseDayTests [Theory] [InlineData("example-input.txt", "21")] - [InlineData("my-input.txt", "", Skip = "Unsolved yet")] + [InlineData("my-input.txt", "7939")] public void TestPart1(string inputFilename, string expectedResult) => BaseTestPart1(inputFilename, expectedResult);