Added solution for "Day 18: Lavaduct Lagoon", part 2

This commit is contained in:
Stefan Müller 2024-07-03 20:41:19 +02:00
parent b086038aa5
commit ba4195af82
3 changed files with 173 additions and 138 deletions

View File

@ -148,6 +148,16 @@ Instead, the solver uses a somewhat Dijkstra-inspired algorithm, where for each
The main modification to the classic algorithm here is that in order to calculate e.g. the horizontal current minimum for a grid point, the vertical current minimum of its vertical neighbors within a certain range have to be considered. The difference between part 1 and 2 is only the specific range to be used.
### Day 18: Lavaduct Lagoon
:star: :mag_right: Puzzle: <https://adventofcode.com/2023/day/18>, :white_check_mark: Solver: [`ULavaductLagoon.pas`](solvers/ULavaductLagoon.pas)
My first algorithm for part 1 was a simply tracking the trench in a top-view two-dimensional array and then flood-filling the outside of the trench to determine the full area. It worked, but there were two problems. Firstly, I had to iteratre over the list of digs twice in order to avoid resizing the array frequently. Secondly, the performance complexity of the algorthim depends largely on the size of the array, i.e. the length of the individual digs, so obviously it did not scale for part2.
The final algorithm, uses the fact that either all right turns are convex or concave, locally, while all left turns are the opposite. That means that two consecutive turns in the same direction (a U-turn) enclose a rectangular area that is either inside or outside of the trench depending only on the direction of the two turns. So the algorthim simply collapses all U-turns it encounters into a straight dig instruction, thereby cutting of an area that is either added to or subtracted from the running area count.
These U-turn collapses are done immediately when adding digs because then the U-turns will always either be at the end of the list or just before the last collapse. One difficulty is that the in order for this to work well, the algorithm needs to ensure that consecutive digs are always perpendicular, merging any that are parallel into a single one.
### Day 19: Aplenty
:mag_right: Puzzle: <https://adventofcode.com/2023/day/19>, :white_check_mark: Solver: [`UAplenty.pas`](solvers/UAplenty.pas)

View File

@ -22,32 +22,38 @@ unit ULavaductLagoon;
interface
uses
Classes, SysUtils, Generics.Collections, USolver, UCommon;
Classes, SysUtils, StrUtils, Generics.Collections, Math, USolver;
type
{ TDig }
TDig = record
Direction: TPoint;
Length: Cardinal;
TDig = class
Direction, Length: Integer;
end;
TDigs = specialize TList<TDig>;
TDigs = specialize TObjectList<TDig>;
{ TDigSite }
TDigSite = class
private
FDigs: TDigs;
FArea, FTrench: Int64;
function CheckMergeDigs(const ADigIndex: Integer): Cardinal;
public
procedure AddDig(constref ADig: TDig);
procedure CollapseUTurns;
function CalcFinalArea: Int64;
constructor Create;
destructor Destroy; override;
end;
{ TLavaductLagoon }
TLavaductLagoon = class(TSolver)
FDigs: TDigs;
FCurrentPosiiton: TPoint;
FDigRect: TRect;
FDigSite: array of array of Boolean;
FHigh: TPoint;
function AddDig(const ALine: string): TDig;
procedure UpdateDigRect(constref ADig: TDig);
procedure CalculateDigSite;
function CalculateLagoonSize: Int64;
function CheckPositionUntouched(const AX, AY: Integer; var ACount: Integer): Boolean;
FSite1, FSite2: TDigSite;
procedure AddDig(const ALine: string);
public
constructor Create;
destructor Destroy; override;
@ -59,154 +65,173 @@ type
implementation
{ TLavaductLagoon }
{ TDigSite }
function TLavaductLagoon.AddDig(const ALine: string): TDig;
var
split: TStringArray;
function TDigSite.CheckMergeDigs(const ADigIndex: Integer): Cardinal;
begin
split := ALine.Split([' ']);
case split[0] of
'R': Result.Direction := CDirectionRight;
'D': Result.Direction := CDirectionDown;
'L': Result.Direction := CDirectionLeft;
'U': Result.Direction := CDirectionUp;
end;
Result.Length := StrToUInt(split[1]);
FDigs.Add(Result);
end;
procedure TLavaductLagoon.UpdateDigRect(constref ADig: TDig);
begin
if ADig.Direction.Y = 0 then
Result := 0;
if (0 <= ADigIndex) and (ADigIndex < FDigs.Count - 1) then
begin
Inc(FCurrentPosiiton.X, ADig.Length * ADig.Direction.X);
if FCurrentPosiiton.X < FDigRect.Left then
FDigRect.Left := FCurrentPosiiton.X;
if FDigRect.Right < FCurrentPosiiton.X then
FDigRect.Right := FCurrentPosiiton.X;
end
else begin
Inc(FCurrentPosiiton.Y, ADig.Length * ADig.Direction.Y);
if FCurrentPosiiton.Y < FDigRect.Top then
FDigRect.Top := FCurrentPosiiton.Y;
if FDigRect.Bottom < FCurrentPosiiton.Y then
FDigRect.Bottom := FCurrentPosiiton.Y;
end;
end;
procedure TLavaductLagoon.CalculateDigSite;
var
i, j: Integer;
dig: TDig;
begin
// Initializes dig site array.
FHigh := FDigRect.BottomRight - FDigRect.TopLeft;
SetLength(FDigSite, FHigh.X + 1, FHigh.Y + 1);
for i := 0 to FHigh.X do
for j := 0 to FHigh.Y do
FDigSite[i, j] := False;
// Initializes start position.
FCurrentPosiiton := Point(-FDigRect.Left, -FDigRect.Top);
FDigSite[FCurrentPosiiton.X, FCurrentPosiiton.Y] := True;
// Applies digs to dig site array.
for dig in FDigs do
for i := 1 to dig.Length do
// Appends two consecutive digs, if they go in the same direction.
if FDigs[ADigIndex].Direction = FDigs[ADigIndex + 1].Direction then
begin
FCurrentPosiiton := FCurrentPosiiton + dig.Direction;
FDigSite[FCurrentPosiiton.X, FCurrentPosiiton.Y] := True;
end;
end;
function TLavaductLagoon.CalculateLagoonSize: Int64;
var
stack: specialize TStack<TPoint>;
exteriors, i: Integer;
position, neighbor: TPoint;
direction: PPoint;
begin
// Counts exterior, untouched positions.
stack := specialize TStack<TPoint>.Create;
exteriors := 0;
// Counts untouched position on the edge of the dig site and their neighbors, and pushes those neighbors on the stack.
// With this we ensure that items on the stack are never on the edge, thus avoiding bounds checks later.
for i := 0 to FHigh.Y do
begin
if CheckPositionUntouched(0, i, exteriors)
and (1 < i) and (i < FHigh.Y)
and CheckPositionUntouched(1, i, exteriors) then
stack.Push(Point(1, i));
if CheckPositionUntouched(FHigh.X, i, exteriors)
and (0 < i) and (i < FHigh.Y - 1)
and CheckPositionUntouched(FHigh.X - 1, i, exteriors) then
stack.Push(Point(FHigh.X - 1, i));
end;
for i := 0 to FHigh.X do
begin
if CheckPositionUntouched(i, 0, exteriors)
and (0 < i) and (i < FHigh.X - 1)
and CheckPositionUntouched(i, 1, exteriors) then
stack.Push(Point(i, 1));
if CheckPositionUntouched(i, FHigh.Y, exteriors)
and (1 < i) and (i < FHigh.X)
and CheckPositionUntouched(i, FHigh.Y - 1, exteriors) then
stack.Push(Point(i, FHigh.Y - 1));
end;
// Counts the remaining exterior area while flood-filling the positions.
while stack.Count > 0 do
begin
position := stack.Pop;
for direction in CPCardinalDirections do
FDigs[ADigIndex].Length := FDigs[ADigIndex].Length + FDigs[ADigIndex + 1].Length;
FDigs.Delete(ADigIndex + 1);
Inc(Result);
end
// Otherwise, checks if the directions are opposite.
else if Abs(FDigs[ADigIndex].Direction - FDigs[ADigIndex + 1].Direction) = 2 then
begin
neighbor := position + direction^;
if CheckPositionUntouched(neighbor.X, neighbor.Y, exteriors) then
stack.Push(neighbor);
// Recurses, if the opposite digs cancel each other out.
if FDigs[ADigIndex].Length = FDigs[ADigIndex + 1].Length then
begin
FDigs.DeleteRange(ADigIndex, 2);
Inc(Result, 2);
Inc(Result, CheckMergeDigs(ADigIndex - 1));
end
else begin
// Otherwise, subtracts the opposite directions.
if FDigs[ADigIndex].Length > FDigs[ADigIndex + 1].Length then
FDigs[ADigIndex].Length := FDigs[ADigIndex].Length - FDigs[ADigIndex + 1].Length
else begin
FDigs[ADigIndex].Length := FDigs[ADigIndex + 1].Length - FDigs[ADigIndex].Length;
FDigs[ADigIndex].Direction := FDigs[ADigIndex + 1].Direction;
end;
FDigs.Delete(ADigIndex + 1);
Inc(Result);
end;
end;
end;
stack.Free;
Result := (FHigh.X + 1) * (FHigh.Y + 1) - exteriors;
end;
function TLavaductLagoon.CheckPositionUntouched(const AX, AY: Integer; var ACount: Integer): Boolean;
procedure TDigSite.AddDig(constref ADig: TDig);
begin
Result := not FDigSite[AX, AY];
if Result then
FDigs.Add(ADig);
Inc(FTrench, ADig.Length);
// The new dig might have to be merged with the preceeding dig.
CheckMergeDigs(FDigs.Count - 2);
end;
procedure TDigSite.CollapseUTurns;
var
i, side, backtrack, shorter: Integer;
begin
// If there is a U-turn, it must involve the last dig, otherwise we would have collapsed it in an earlier call
// already. Therefore we check the last three digs first.
i := FDigs.Count - 3;
while (0 <= i) and (i + 2 < FDigs.Count) do
begin
FDigSite[AX, AY] := True;
Inc(ACount);
// We check if three consecutive digs starting at i form a U-turn. It's enough to check whether the first and the
// third have different directions because they must be parallel.
if FDigs[i].Direction <> FDigs[i + 2].Direction then
begin
// Either right or left U-turns enclose an area inside the trench. We do not need to know which one is which and
// just assume here that the right U-turns enclose an area outside and therefore negate it. If it's the other way
// around, then we can simply negate the end result.
side := FDigs[i + 1].Length;
if (FDigs[i].Direction + 1 = FDigs[i + 1].Direction)
or (FDigs[i + 1].Direction + 1 = FDigs[i + 2].Direction) then
side := -side;
// Updates the shortened dig and collapses the U-turn.
backtrack := 0;
shorter := Min(FDigs[i].Length, FDigs[i + 2].Length);
Inc(FArea, side * shorter);
if FDigs[i + 2].Length = shorter then
begin
FDigs.Delete(i + 2);
Inc(backtrack);
Inc(backtrack, CheckMergeDigs(i + 1));
end
else
Dec(FDigs[i + 2].Length, shorter);
if FDigs[i].Length = shorter then
begin
FDigs.Delete(i);
Inc(backtrack);
Inc(backtrack, CheckMergeDigs(i - 1));
end
else
Dec(FDigs[i].Length, shorter);
Dec(i, backtrack);
end
else
Inc(i);
end;
end;
constructor TLavaductLagoon.Create;
function TDigSite.CalcFinalArea: Int64;
begin
// If the area is negative, then outside and inside have to be "swapped", which Abs() achieves here.
// When collapsing the U-turns, only the area up to the imaginary middle line of the dug trench is considered, so half
// of the full length of the dug trench + 1 has to be added to include the whole trench in the area calculation.
Result := Abs(FArea) + FTrench div 2 + 1;
end;
constructor TDigSite.Create;
begin
FDigs := TDigs.Create;
FCurrentPosiiton := Point(0, 0);
FDigRect := Rect(0, 0, 0, 0);
end;
destructor TLavaductLagoon.Destroy;
destructor TDigSite.Destroy;
begin
FDigs.Free;
inherited Destroy;
end;
procedure TLavaductLagoon.ProcessDataLine(const ALine: string);
{ TLavaductLagoon }
procedure TLavaductLagoon.AddDig(const ALine: string);
var
split: TStringArray;
dig: TDig;
begin
dig := AddDig(ALine);
UpdateDigRect(dig);
dig := TDig.Create;
split := ALine.Split([' ']);
case split[0] of
'R': dig.Direction := 0;
'D': dig.Direction := 1;
'L': dig.Direction := 2;
'U': dig.Direction := 3;
end;
dig.Length := StrToInt(split[1]);
FSite1.AddDig(dig);
dig := TDig.Create;
dig.Direction := StrToInt(split[2][8]);
dig.Length := Hex2Dec(Copy(split[2], 3, 5));
FSite2.AddDig(dig);
end;
constructor TLavaductLagoon.Create;
begin
FSite1 := TDigSite.Create;
FSite2 := TDigSite.Create;
end;
destructor TLavaductLagoon.Destroy;
begin
FSite1.Free;
FSite2.Free;
inherited Destroy;
end;
procedure TLavaductLagoon.ProcessDataLine(const ALine: string);
begin
AddDig(ALine);
FSite1.CollapseUTurns;
FSite2.CollapseUTurns;
end;
procedure TLavaductLagoon.Finish;
begin
CalculateDigSite;
FPart1 := CalculateLagoonSize;
// If the area is negative, then outside and inside have to be "swapped", which Abs() achieves here.
// When collapsing the U-turns, only the area up to the imaginary middle line of the dug trench is considered, so half
// of the full length of the dug trench + 1 has to be added to include the whole trench in the area calculation.
FPart1 := FSite1.CalcFinalArea;
FPart2 := FSite2.CalcFinalArea;
end;
function TLavaductLagoon.GetDataFileName: string;

View File

@ -33,7 +33,7 @@ type
function CreateSolver: ISolver; override;
published
procedure TestPart1;
//procedure TestPart2;
procedure TestPart2;
end;
implementation
@ -50,10 +50,10 @@ begin
AssertEquals(62, FSolver.GetResultPart1);
end;
//procedure TLavaductLagoonExampleTestCase.TestPart2;
//begin
// AssertEquals(-1, FSolver.GetResultPart2);
//end;
procedure TLavaductLagoonExampleTestCase.TestPart2;
begin
AssertEquals(952408144115, FSolver.GetResultPart2);
end;
initialization