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