From 3c35f830dabedfac3c88da925b97852789babae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Moru=C5=9B?= Date: Thu, 18 Jul 2024 17:22:55 +0200 Subject: [PATCH] Solved Day12 puzzle (#50) * First boilerplate day 12 code * Tweak restore inputs script so that example inputs are copied back from tests * Update and add missing .run files * Finished part 1 of the puzzle * Solved part two * Fix formatting --- .run/Day01 example 1.run.xml | 20 +++ .run/Day01 example 2.run.xml | 20 +++ .run/Day01.run.xml | 20 +++ .run/Day02 example.run.xml | 20 +++ .run/Day02.run.xml | 20 +++ .run/Day03 example.run.xml | 20 +++ .run/Day03.run.xml | 20 +++ .run/Day04 example.run.xml | 20 +++ .run/Day04.run.xml | 20 +++ .run/Day05 example.run.xml | 20 +++ .run/Day05.run.xml | 20 +++ .run/Day06 example.run.xml | 20 +++ .run/Day06.run.xml | 20 +++ .run/Day10 example 6.run.xml | 37 ++-- .run/Day10 example 7.run.xml | 37 ++-- .run/Day10 example 8.run.xml | 37 ++-- .run/Day12 example.run.xml | 20 +++ .run/Day12.run.xml | 20 +++ AdventOfCode2023.sln | 6 + Day12 - Hot Springs/.vscode/launch.json | 18 ++ Day12 - Hot Springs/.vscode/tasks.json | 21 +++ Day12 - Hot Springs/Day12.csproj | 22 +++ Day12 - Hot Springs/Day12Solver.cs | 75 ++++++++ Day12 - Hot Springs/Day12SolverOptions.cs | 8 + Day12 - Hot Springs/InputReader.cs | 78 +++++++++ Day12 - Hot Springs/Program.cs | 45 +++++ Day12 - Hot Springs/README.md | 1 + .../SpringArrangementCounter.cs | 162 ++++++++++++++++++ Day12 - Hot Springs/SpringArrangementMemo.cs | 31 ++++ Day12 - Hot Springs/SpringCondition.cs | 8 + Day12 - Hot Springs/SpringRow.cs | 7 + Tests/Day12Tests.cs | 24 +++ Tests/Inputs/Day12/example-input.txt | 6 + Tests/Tests.csproj | 11 +- restore-inputs.ps1 | 8 + 35 files changed, 883 insertions(+), 59 deletions(-) create mode 100644 .run/Day01 example 1.run.xml create mode 100644 .run/Day01 example 2.run.xml create mode 100644 .run/Day01.run.xml create mode 100644 .run/Day02 example.run.xml create mode 100644 .run/Day02.run.xml create mode 100644 .run/Day03 example.run.xml create mode 100644 .run/Day03.run.xml create mode 100644 .run/Day04 example.run.xml create mode 100644 .run/Day04.run.xml create mode 100644 .run/Day05 example.run.xml create mode 100644 .run/Day05.run.xml create mode 100644 .run/Day06 example.run.xml create mode 100644 .run/Day06.run.xml create mode 100644 .run/Day12 example.run.xml create mode 100644 .run/Day12.run.xml create mode 100644 Day12 - Hot Springs/.vscode/launch.json create mode 100644 Day12 - Hot Springs/.vscode/tasks.json create mode 100644 Day12 - Hot Springs/Day12.csproj create mode 100644 Day12 - Hot Springs/Day12Solver.cs create mode 100644 Day12 - Hot Springs/Day12SolverOptions.cs create mode 100644 Day12 - Hot Springs/InputReader.cs create mode 100644 Day12 - Hot Springs/Program.cs create mode 100644 Day12 - Hot Springs/README.md 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 create mode 100644 Tests/Day12Tests.cs create mode 100644 Tests/Inputs/Day12/example-input.txt diff --git a/.run/Day01 example 1.run.xml b/.run/Day01 example 1.run.xml new file mode 100644 index 0000000..c3202e8 --- /dev/null +++ b/.run/Day01 example 1.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day01 example 2.run.xml b/.run/Day01 example 2.run.xml new file mode 100644 index 0000000..9daee01 --- /dev/null +++ b/.run/Day01 example 2.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day01.run.xml b/.run/Day01.run.xml new file mode 100644 index 0000000..b742b33 --- /dev/null +++ b/.run/Day01.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day02 example.run.xml b/.run/Day02 example.run.xml new file mode 100644 index 0000000..69106f7 --- /dev/null +++ b/.run/Day02 example.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day02.run.xml b/.run/Day02.run.xml new file mode 100644 index 0000000..cf05699 --- /dev/null +++ b/.run/Day02.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day03 example.run.xml b/.run/Day03 example.run.xml new file mode 100644 index 0000000..bf9215f --- /dev/null +++ b/.run/Day03 example.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day03.run.xml b/.run/Day03.run.xml new file mode 100644 index 0000000..bda1ac4 --- /dev/null +++ b/.run/Day03.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day04 example.run.xml b/.run/Day04 example.run.xml new file mode 100644 index 0000000..70aa4d8 --- /dev/null +++ b/.run/Day04 example.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day04.run.xml b/.run/Day04.run.xml new file mode 100644 index 0000000..6eec148 --- /dev/null +++ b/.run/Day04.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day05 example.run.xml b/.run/Day05 example.run.xml new file mode 100644 index 0000000..a44dca9 --- /dev/null +++ b/.run/Day05 example.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day05.run.xml b/.run/Day05.run.xml new file mode 100644 index 0000000..70b8279 --- /dev/null +++ b/.run/Day05.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day06 example.run.xml b/.run/Day06 example.run.xml new file mode 100644 index 0000000..5a2101c --- /dev/null +++ b/.run/Day06 example.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day06.run.xml b/.run/Day06.run.xml new file mode 100644 index 0000000..4b3713f --- /dev/null +++ b/.run/Day06.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day10 example 6.run.xml b/.run/Day10 example 6.run.xml index 41eaa85..0118012 100644 --- a/.run/Day10 example 6.run.xml +++ b/.run/Day10 example 6.run.xml @@ -1,21 +1,20 @@  - - + + diff --git a/.run/Day10 example 7.run.xml b/.run/Day10 example 7.run.xml index 506b8c8..7137975 100644 --- a/.run/Day10 example 7.run.xml +++ b/.run/Day10 example 7.run.xml @@ -1,21 +1,20 @@  - - + + diff --git a/.run/Day10 example 8.run.xml b/.run/Day10 example 8.run.xml index 8f78360..b0b3729 100644 --- a/.run/Day10 example 8.run.xml +++ b/.run/Day10 example 8.run.xml @@ -1,21 +1,20 @@  - - + + diff --git a/.run/Day12 example.run.xml b/.run/Day12 example.run.xml new file mode 100644 index 0000000..d3bcaed --- /dev/null +++ b/.run/Day12 example.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/Day12.run.xml b/.run/Day12.run.xml new file mode 100644 index 0000000..9603656 --- /dev/null +++ b/.run/Day12.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/AdventOfCode2023.sln b/AdventOfCode2023.sln index dbefd4f..8083a9e 100644 --- a/AdventOfCode2023.sln +++ b/AdventOfCode2023.sln @@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Day10", "Day10 - Pipe Maze\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Day11", "Day11 - Cosmic Expansion\Day11.csproj", "{28DC51A6-39A7-4180-B890-D5B5D0AF71EC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Day12", "Day12 - Hot Springs\Day12.csproj", "{588B94EB-1D61-49FA-ADB2-22DD0A83A172}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -98,6 +100,10 @@ Global {28DC51A6-39A7-4180-B890-D5B5D0AF71EC}.Debug|Any CPU.Build.0 = Debug|Any CPU {28DC51A6-39A7-4180-B890-D5B5D0AF71EC}.Release|Any CPU.ActiveCfg = Release|Any CPU {28DC51A6-39A7-4180-B890-D5B5D0AF71EC}.Release|Any CPU.Build.0 = Release|Any CPU + {588B94EB-1D61-49FA-ADB2-22DD0A83A172}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {588B94EB-1D61-49FA-ADB2-22DD0A83A172}.Debug|Any CPU.Build.0 = Debug|Any CPU + {588B94EB-1D61-49FA-ADB2-22DD0A83A172}.Release|Any CPU.ActiveCfg = Release|Any CPU + {588B94EB-1D61-49FA-ADB2-22DD0A83A172}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {CE8086B8-A3E4-40E0-859F-607F95D25514} = {F6C05A63-6269-425F-B877-9E9F0BC1FC26} diff --git a/Day12 - Hot Springs/.vscode/launch.json b/Day12 - Hot Springs/.vscode/launch.json new file mode 100644 index 0000000..7b4b93c --- /dev/null +++ b/Day12 - Hot Springs/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch (my input)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/net8.0/Day12.dll", + "args": [ + "input.txt" + ], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + } + ] +} diff --git a/Day12 - Hot Springs/.vscode/tasks.json b/Day12 - Hot Springs/.vscode/tasks.json new file mode 100644 index 0000000..18c16a6 --- /dev/null +++ b/Day12 - Hot Springs/.vscode/tasks.json @@ -0,0 +1,21 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "group": { + "kind": "build", + "isDefault": true + }, + "args": [ + "build", + "${workspaceFolder}/Day12.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/Day12 - Hot Springs/Day12.csproj b/Day12 - Hot Springs/Day12.csproj new file mode 100644 index 0000000..a643ece --- /dev/null +++ b/Day12 - Hot Springs/Day12.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + AdventOfCode.Year2023.Day12 + + + + + + + + + + Always + + + + diff --git a/Day12 - Hot Springs/Day12Solver.cs b/Day12 - Hot Springs/Day12Solver.cs new file mode 100644 index 0000000..49af522 --- /dev/null +++ b/Day12 - Hot Springs/Day12Solver.cs @@ -0,0 +1,75 @@ +using AdventOfCode.Abstractions; + +namespace AdventOfCode.Year2023.Day12; + +public sealed class Day12Solver : DaySolver +{ + public override int Year => 2023; + public override int Day => 12; + public override string Title => "Hot Springs"; + + private readonly IReadOnlyList _rows; + private readonly int _unfoldRepetitions; + + public Day12Solver(Day12SolverOptions options) : base(options) + { + var inputReader = new InputReader(); + _rows = inputReader.ReadInput(InputLines); + _unfoldRepetitions = options.UnfoldRepetitions; + } + + public Day12Solver(Action configure) + : this(DaySolverOptions.FromConfigureAction(configure)) + { + } + + public Day12Solver() : this(new Day12SolverOptions()) + { + } + + public override string SolvePart1() + { + var counter = new SpringArrangementCounter(); + long result = 0; + foreach (var row in _rows) + { + result += counter.CountPossibleArrangements(row); + } + + return result.ToString(); + } + + public override string SolvePart2() + { + var counter = new SpringArrangementCounter(); + long result = 0; + foreach (var row in _rows) + { + var unfoldedRow = UnfoldRow(row); + result += counter.CountPossibleArrangements(unfoldedRow); + } + + return result.ToString(); + } + + private SpringRow UnfoldRow(SpringRow springRow) + { + var newConditionRecords = new SpringCondition[springRow.ConditionRecords.Length * _unfoldRepetitions + _unfoldRepetitions - 1]; + springRow.ConditionRecords.CopyTo(newConditionRecords, 0); + for (int i = 1; i < _unfoldRepetitions; i++) + { + int destination = i * (springRow.ConditionRecords.Length + 1); + newConditionRecords[destination - 1] = SpringCondition.Unknown; + springRow.ConditionRecords.CopyTo(newConditionRecords, destination); + } + + var newGroupSizes = new int[springRow.DamagedGroupSizes.Length * _unfoldRepetitions]; + for (int i = 0; i < _unfoldRepetitions; i++) + { + int destination = i * springRow.DamagedGroupSizes.Length; + springRow.DamagedGroupSizes.CopyTo(newGroupSizes, destination); + } + + return new(newConditionRecords, newGroupSizes); + } +} diff --git a/Day12 - Hot Springs/Day12SolverOptions.cs b/Day12 - Hot Springs/Day12SolverOptions.cs new file mode 100644 index 0000000..a8bff23 --- /dev/null +++ b/Day12 - Hot Springs/Day12SolverOptions.cs @@ -0,0 +1,8 @@ +using AdventOfCode.Abstractions; + +namespace AdventOfCode.Year2023.Day12; + +public sealed class Day12SolverOptions : DaySolverOptions +{ + public int UnfoldRepetitions { get; set; } = 5; +} diff --git a/Day12 - Hot Springs/InputReader.cs b/Day12 - Hot Springs/InputReader.cs new file mode 100644 index 0000000..a04de22 --- /dev/null +++ b/Day12 - Hot Springs/InputReader.cs @@ -0,0 +1,78 @@ +using System.Text.RegularExpressions; +using AdventOfCode.Common.SpanExtensions; + +namespace AdventOfCode.Year2023.Day12; + +internal sealed class InputReader +{ + 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 SpringRow[] ReadInput(IEnumerable inputLines) + { + List rows = new(1000); + foreach (string line in inputLines) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + var match = SpringRowRegex.Match(line); + if (!match.Success) + { + throw new InputException($"Invalid input line ('{line}')"); + } + + var springsSpan = match.Groups[1].ValueSpan; + var springs = ReadSprings(springsSpan); + + var springGroupSizesSpan = match.Groups[2].ValueSpan; + var springGroupSizes = ReadSpringGroupSizes(springGroupSizesSpan); + + rows.Add(new SpringRow(springs, springGroupSizes)); + } + + return rows.ToArray(); + } + + private SpringCondition[] ReadSprings(ReadOnlySpan springs) + { + springs = springs.Trim(); + var result = new SpringCondition[springs.Length]; + for (int i = 0; i < springs.Length; i++) + { + result[i] = ParseSpringType(springs[i]); + } + + return result; + } + + private static SpringCondition ParseSpringType(char c) + => c switch + { + OperationalSpringChar => SpringCondition.Operational, + DamagedSpringChar => SpringCondition.Damaged, + UnknownSpringChar => SpringCondition.Unknown, + _ => throw new InputException($"Invalid spring type character ('{c}')") + }; + + private int[] ReadSpringGroupSizes(ReadOnlySpan springGroupSizes) + { + int count = springGroupSizes.Count(','); + var result = new List(count + 1); + foreach (var part in springGroupSizes.Split(',')) + { + if (!int.TryParse(part, out int size)) + { + throw new InputException($"Invalid spring group size ('{part}')"); + } + result.Add(size); + } + + return result.ToArray(); + } +} diff --git a/Day12 - Hot Springs/Program.cs b/Day12 - Hot Springs/Program.cs new file mode 100644 index 0000000..8f4c6f2 --- /dev/null +++ b/Day12 - Hot Springs/Program.cs @@ -0,0 +1,45 @@ +using System.Diagnostics; +using AdventOfCode; +using AdventOfCode.Year2023.Day12; + +try +{ + string? filepath = args.Length switch + { + 0 => null, + 1 => args[0], + _ => throw new CommandLineException( + $"Program was called with too many arguments. Proper usage: \"dotnet run []\"." + ) + }; + + Day12Solver solver = new(options => + { + options.InputFilepath = filepath ?? options.InputFilepath; + }); + + Console.WriteLine($"--- Day {solver.Day}: {solver.Title} ---"); + + Console.Write("Part one: "); + string part1 = solver.SolvePart1(); + Console.WriteLine(part1); + + Console.Write("Part two: "); + string part2 = solver.SolvePart2(); + Console.WriteLine(part2); +} +catch (AdventOfCodeException e) +{ + string errorPrefix = e switch + { + CommandLineException => "Command line error", + InputException => "Input error", + DaySolverException => "Day solver error", + _ => throw new UnreachableException($"Unknown exception type \"{e.GetType()}\".") + }; + + Console.ForegroundColor = ConsoleColor.Red; + Console.Error.WriteLine($"{errorPrefix}: {e.Message}"); + Console.ResetColor(); + Environment.Exit(1); +} diff --git a/Day12 - Hot Springs/README.md b/Day12 - Hot Springs/README.md new file mode 100644 index 0000000..9e92340 --- /dev/null +++ b/Day12 - Hot Springs/README.md @@ -0,0 +1 @@ +# [Day 12: Hot Springs](https://adventofcode.com/2023/day/12) diff --git a/Day12 - Hot Springs/SpringArrangementCounter.cs b/Day12 - Hot Springs/SpringArrangementCounter.cs new file mode 100644 index 0000000..f889c3d --- /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 long 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 long 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); + } + + long 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 long 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 long 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 long 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..9b3eb9c --- /dev/null +++ b/Day12 - Hot Springs/SpringArrangementMemo.cs @@ -0,0 +1,31 @@ +namespace AdventOfCode.Year2023.Day12; + +internal sealed class SpringArrangementMemo +{ + private long[,,] _memo = new long[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 long[newSpringsDimension, newGroupsDimension, newGroupSizeDimension]; + } + else + { + Array.Clear(_memo); + } + } + + public long 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..304d06d --- /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..add0c89 --- /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 new file mode 100644 index 0000000..a76f876 --- /dev/null +++ b/Tests/Day12Tests.cs @@ -0,0 +1,24 @@ +using AdventOfCode.Year2023.Day12; + +namespace AdventOfCode.Year2023.Tests; + +[Trait("Year", "2023")] +[Trait("Day", "12")] +public sealed class Day12Tests : BaseDayTests +{ + protected override string DayInputsDirectory => "Day12"; + + protected override Day12Solver CreateSolver(Day12SolverOptions options) => new(options); + + [Theory] + [InlineData("example-input.txt", "21")] + [InlineData("my-input.txt", "7939")] + public void TestPart1(string inputFilename, string expectedResult) + => BaseTestPart1(inputFilename, expectedResult); + + [Theory] + [InlineData("example-input.txt", "525152")] + [InlineData("my-input.txt", "850504257483930")] + public void TestPart2(string inputFilename, string expectedResult) + => BaseTestPart2(inputFilename, expectedResult); +} diff --git a/Tests/Inputs/Day12/example-input.txt b/Tests/Inputs/Day12/example-input.txt new file mode 100644 index 0000000..28ac192 --- /dev/null +++ b/Tests/Inputs/Day12/example-input.txt @@ -0,0 +1,6 @@ +???.### 1,1,3 +.??..??...?##. 1,1,3 +?#?#?#?#?#?#?#? 1,3,1,6 +????.#...#... 4,1,1 +????.######..#####. 1,6,5 +?###???????? 3,2,1 diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 335cbf3..43d98fd 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -37,10 +37,17 @@ - + - + + + + + + + Always + diff --git a/restore-inputs.ps1 b/restore-inputs.ps1 index 6a594ea..cbe47e2 100644 --- a/restore-inputs.ps1 +++ b/restore-inputs.ps1 @@ -87,6 +87,14 @@ foreach ($dayDirectory in $dayDirectories) { $testsInputRelativePath = Resolve-Path -Path $testsInputCreatedDirectory -Relative -RelativeBasePath $originalDirectory Write-Information " Tests directory was not found, created at $testsInputRelativePath" } + else { + # If directory existed, copy back potential example input files in them + $exampleInputFiles = Get-ChildItem -Path $testsInputDirectoryPath -Filter "example-input*.txt" + foreach ($exampleInputFile in $exampleInputFiles) { + $newExampleInputFileName = $exampleInputFile.Name -replace "^example-input-?", "example" + Copy-Item -Path $exampleInputFile.FullName -Destination $newExampleInputFileName + } + } $testsInputPath = Join-Path $testsInputDirectoryPath $testInputFilename if (Test-Path $testsInputPath) { $testFileHash = (Get-FileHash $testsInputPath -Algorithm MD5).Hash