Skip to content

Commit

Permalink
Finished part 1 of the puzzle
Browse files Browse the repository at this point in the history
  • Loading branch information
mMosiur committed Jul 18, 2024
1 parent 52c11ab commit 2ffa282
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 55 deletions.
7 changes: 4 additions & 3 deletions Day12 - Hot Springs/Day12Solver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ public sealed class Day12Solver : DaySolver
public override int Day => 12;
public override string Title => "Hot Springs";

private readonly IReadOnlyList<InputRow> _rows;
private readonly IReadOnlyList<SpringRow> _rows;

public Day12Solver(Day12SolverOptions options) : base(options)
{
var inputReader = new InputReader(options);
var inputReader = new InputReader();
_rows = inputReader.ReadInput(InputLines);
}

Expand All @@ -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();
}

Expand Down
3 changes: 0 additions & 3 deletions Day12 - Hot Springs/Day12SolverOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; } = '?';
}
65 changes: 17 additions & 48 deletions Day12 - Hot Springs/InputReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> inputLines)
{
_operationalSpringChar = options.OperationalSpringChar;
_damagedSpringChar = options.DamagedSpringChar;
_unknownSpringChar = options.UnknownSpringChar;
}

public IReadOnlyList<InputRow> ReadInput(IEnumerable<string> inputLines)
{
List<InputRow> rows = new(1000);
List<SpringRow> rows = new(1000);
foreach (string line in inputLines)
{
if (string.IsNullOrWhiteSpace(line))
Expand All @@ -40,13 +33,13 @@ public IReadOnlyList<InputRow> ReadInput(IEnumerable<string> 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<SpringCondition> ReadSprings(ReadOnlySpan<char> springs)
private SpringCondition[] ReadSprings(ReadOnlySpan<char> springs)
{
springs = springs.Trim();
var result = new SpringCondition[springs.Length];
Expand All @@ -58,27 +51,16 @@ private IReadOnlyList<SpringCondition> ReadSprings(ReadOnlySpan<char> 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<int> ReadSpringGroupSizes(ReadOnlySpan<char> springGroupSizes)
private int[] ReadSpringGroupSizes(ReadOnlySpan<char> springGroupSizes)
{
int count = springGroupSizes.Count(',');
var result = new List<int>(count + 1);
Expand All @@ -91,19 +73,6 @@ private IReadOnlyList<int> ReadSpringGroupSizes(ReadOnlySpan<char> springGroupSi
result.Add(size);
}

return result;
return result.ToArray();
}
}

internal enum SpringCondition
{
Unknown,
Operational,
Damaged,
}

internal class InputRow(IReadOnlyList<SpringCondition> springs, IReadOnlyList<int> springGroupSizes)
{
public IReadOnlyList<SpringCondition> Springs { get; } = springs;
public IReadOnlyList<int> SpringGroupSizes { get; } = springGroupSizes;
}
162 changes: 162 additions & 0 deletions Day12 - Hot Springs/SpringArrangementCounter.cs
Original file line number Diff line number Diff line change
@@ -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];
}

/// <summary>
/// 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).
/// </summary>
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;
}

/// <summary>
/// Calculates values for base cases of first spring at index 0, does not use memo as it is not needed.
/// </summary>
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}")
};
}

/// <summary>
/// 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.
/// </summary>
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];
}

/// <summary>
/// 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.
/// </summary>
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];
}

/// <summary>
/// Returns whether <paramref name="springSpan"/> contains any damaged springs.
/// </summary>
private static bool ContainsDamaged(ReadOnlySpan<SpringCondition> springSpan)
{
foreach (var item in springSpan)
{
if (item is SpringCondition.Damaged)
{
return true;
}
}

return false;
}
}
31 changes: 31 additions & 0 deletions Day12 - Hot Springs/SpringArrangementMemo.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
8 changes: 8 additions & 0 deletions Day12 - Hot Springs/SpringCondition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace AdventOfCode.Year2023.Day12;

internal enum SpringCondition
{
Unknown,
Operational,
Damaged,
}
7 changes: 7 additions & 0 deletions Day12 - Hot Springs/SpringRow.cs
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion Tests/Day12Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public sealed class Day12Tests : BaseDayTests<Day12Solver, Day12SolverOptions>

[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);

Expand Down

0 comments on commit 2ffa282

Please sign in to comment.