Added solution for "Day 17: Clumsy Crucible", part 2

This commit is contained in:
Stefan Müller 2024-06-25 22:22:52 +02:00
parent ba1cefc371
commit 7b33e8b406
2 changed files with 298 additions and 139 deletions

View File

@ -22,7 +22,7 @@ unit UClumsyCrucible;
interface
uses
Classes, SysUtils, Generics.Collections, Math, USolver;
Classes, SysUtils, Generics.Collections, Math, USolver, UCommon;
type
@ -39,27 +39,58 @@ type
NeedsUpdate: Boolean;
end;
TAxisId = (axHorizontal, axVertical);
const
CAxisDirections: array[TAxisId] of array[0..1] of PPoint
= ((@CDirectionRight, @CDirectionLeft), (@CDirectionDown, @CDirectionUp));
COtherAxes: array[TAxisId] of TAxisId = (axVertical, axHorizontal);
type
{ TNode }
TNode = record
Horizontal, Vertical: TAxisData;
Axes: array[TAxisId] of TAxisData;
LocalHeatLoss: Byte;
end;
PNode = ^TNode;
TNodeArray = array of TNode;
TNodeArrays = specialize TList<TNodeArray>;
TWorkQueue = specialize TQueue<TPoint>;
{ TNodeMap }
TNodeMap = class
private
// Each item in FNodes is a horizontal row of nodes.
FNodes: TNodeArrays;
FWidth: Integer;
FMinStraight, FMaxStraight: Integer;
function GetHeight: Integer;
function GetNode(APosition: TPoint): TNode;
function GetPNode(APosition: TPoint): PNode;
function IsPositionInMap(constref APosition: TPoint): Boolean;
procedure ClampPositionToMap(var APosition: TPoint);
procedure InitWorkQueue(constref AWorkQueue: TWorkQueue);
procedure InvalidateNeighbors(constref AWorkQueue: TWorkQueue; const AAxis: TAxisId; constref APosition: TPoint);
function FindStepNodeMinimum(const AAxis: TAxisId; constref APosition: TPoint): Cardinal;
public
property Width: Integer read FWidth;
property Height: Integer read GetHeight;
constructor Create;
destructor Destroy; override;
procedure AddNodes(const ALine: string);
function FindMinimumPathLength(const AMinStraight, AMaxStraight: Integer): Cardinal;
procedure Reset;
end;
{ TClumsyCrucible }
TClumsyCrucible = class(TSolver)
private
// Each item in FMap is a horizontal row of nodes.
FMap: TNodeArrays;
FWidth: Integer;
procedure InvalidateHorizontalNeighbors(constref APosition: TPoint; constref AWorkQueue: TWorkQueue);
procedure InvalidateVerticalNeighbors(constref APosition: TPoint; constref AWorkQueue: TWorkQueue);
FMap: TNodeMap;
public
constructor Create;
destructor Destroy; override;
@ -72,39 +103,227 @@ type
implementation
const
CMinStraight = 1;
CMaxStraight = 3;
CUltraMinStraight = 4;
CUltraMaxStraight = 10;
{ TNodeMap }
function TNodeMap.GetHeight: Integer;
begin
Result := FNodes.Count;
end;
function TNodeMap.GetNode(APosition: TPoint): TNode;
begin
Result := FNodes[APosition.Y][APosition.X];
end;
function TNodeMap.GetPNode(APosition: TPoint): PNode;
begin
Result := @FNodes[APosition.Y][APosition.X];
end;
function TNodeMap.IsPositionInMap(constref APosition: TPoint): Boolean;
begin
Result := (0 <= APosition.X) and (APosition.X < Width) and (0 <= APosition.Y) and (APosition.Y < Height);
end;
procedure TNodeMap.ClampPositionToMap(var APosition: TPoint);
begin
if APosition.X < -1 then
APosition.X := -1
else if APosition.X > Width then
APosition.X := Width;
if APosition.Y < -1 then
APosition.Y := -1
else if APosition.Y > Height then
APosition.Y := Height;
end;
procedure TNodeMap.InitWorkQueue(constref AWorkQueue: TWorkQueue);
var
position: TPoint;
last: PNode;
axis: TAxisId;
begin
// Initializes the end node and the work queue with its neighbors.
position := Point(Width - 1, Height - 1);
last := GetPNode(position);
for axis in TAxisId do
begin
last^.Axes[axis].Minimum := 0;
last^.Axes[axis].IsTraversed := True;
end;
InvalidateNeighbors(AWorkQueue, axHorizontal, position);
InvalidateNeighbors(AWorkQueue, axVertical, position);
end;
procedure TNodeMap.InvalidateNeighbors(constref AWorkQueue: TWorkQueue; const AAxis: TAxisId; constref APosition:
TPoint);
var
otherAxis: TAxisId;
nodeMinimum: Cardinal;
direction: PPoint;
neighborPos, stop: TPoint;
neighbor: PNode;
begin
otherAxis := COtherAxes[AAxis];
nodeMinimum := GetNode(APosition).Axes[otherAxis].Minimum;
for direction in CAxisDirections[AAxis] do
begin
neighborPos := Point(APosition.X + direction^.X * FMinStraight, APosition.Y + direction^.Y * FMinStraight);
if IsPositionInMap(neighborPos) then
begin
stop := Point(APosition.X + direction^.X * (FMaxStraight + 1), APosition.Y + direction^.Y * (FMaxStraight + 1));
ClampPositionToMap(stop);
while neighborPos <> stop do
begin
neighbor := GetPNode(neighborPos);
if not neighbor^.Axes[AAxis].NeedsUpdate
and (not neighbor^.Axes[AAxis].IsTraversed or (neighbor^.Axes[AAxis].Minimum > nodeMinimum)) then
begin
neighbor^.Axes[AAxis].NeedsUpdate := True;
if not neighbor^.Axes[otherAxis].NeedsUpdate then
AWorkQueue.Enqueue(neighborPos);
end;
neighborPos := neighborPos + direction^;
end;
end;
end;
end;
function TNodeMap.FindStepNodeMinimum(const AAxis: TAxisId; constref APosition: TPoint): Cardinal;
var
otherAxis: TAxisId;
direction: PPoint;
acc: Cardinal;
neighborPos, start, stop: TPoint;
isStartReached: Boolean;
neighbor: TNode;
begin
otherAxis := COtherAxes[AAxis];
Result := Cardinal.MaxValue;
for direction in CAxisDirections[AAxis] do
begin
acc := 0;
isStartReached := False;
neighborPos := APosition + direction^;
start := Point(APosition.X + direction^.X * FMinStraight, APosition.Y + direction^.Y * FMinStraight);
if IsPositionInMap(start) then
begin
stop := Point(APosition.X + direction^.X * (FMaxStraight + 1), APosition.Y + direction^.Y * (FMaxStraight + 1));
ClampPositionToMap(stop);
while neighborPos <> stop do
begin
if neighborPos = start then
isStartReached := True;
neighbor := GetNode(neighborPos);
Inc(acc, neighbor.LocalHeatLoss);
if isStartReached and neighbor.Axes[otherAxis].IsTraversed then
Result := Min(Result, neighbor.Axes[otherAxis].Minimum + acc);
neighborPos := neighborPos + direction^;
end;
end;
end;
end;
constructor TNodeMap.Create;
begin
FNodes := TNodeArrays.Create;
end;
destructor TNodeMap.Destroy;
begin
FNodes.Free;
inherited Destroy;
end;
procedure TNodeMap.AddNodes(const ALine: string);
var
i: Integer;
nodes: TNodeArray;
axis: TAxisId;
begin
FWidth := Length(ALine);
SetLength(nodes, FWidth);
for i := 0 to FWidth - 1 do
begin
nodes[i].LocalHeatLoss := StrToInt(ALine[i + 1]);
for axis in TAxisId do
begin
nodes[i].Axes[axis].IsTraversed := False;
nodes[i].Axes[axis].NeedsUpdate := False;
end;
end;
FNodes.Add(nodes);
end;
function TNodeMap.FindMinimumPathLength(const AMinStraight, AMaxStraight: Integer): Cardinal;
var
queue: TWorkQueue;
position: TPoint;
node: PNode;
axis: TAxisId;
start: TNode;
newMinimum: Cardinal;
begin
FMinStraight := AMinStraight;
FMaxStraight := AMaxStraight;
queue := TWorkQueue.Create;
InitWorkQueue(queue);
// Processes work queue.
while queue.Count > 0 do
begin
position := queue.Dequeue;
node := GetPNode(position);
for axis in TAxisId do
if node^.Axes[axis].NeedsUpdate then
begin
node^.Axes[axis].NeedsUpdate := False;
// Finds minimum for one step from this node along this axis.
newMinimum := FindStepNodeMinimum(axis, position);
if not node^.Axes[axis].IsTraversed or (node^.Axes[axis].Minimum > newMinimum) then
begin
// Updates this axis minimum and queues update for neighbors on the other axis.
node^.Axes[axis].IsTraversed := True;
node^.Axes[axis].Minimum := newMinimum;
InvalidateNeighbors(queue, COtherAxes[axis], position);
end;
end;
end;
queue.Free;
start := GetNode(Point(0, 0));
Result := Min(start.Axes[axHorizontal].Minimum, start.Axes[axVertical].Minimum);
end;
procedure TNodeMap.Reset;
var
i, j: Integer;
axis: TAxisId;
begin
for i := 0 to Width - 1 do
for j := 0 to Height - 1 do
for axis in TAxisId do
begin
FNodes[j][i].Axes[axis].IsTraversed := False;
FNodes[j][i].Axes[axis].NeedsUpdate := False;
end;
end;
{ TClumsyCrucible }
procedure TClumsyCrucible.InvalidateHorizontalNeighbors(constref APosition: TPoint; constref AWorkQueue: TWorkQueue);
var
i: Integer;
begin
for i := Min(FWidth - 1, APosition.X + CMaxStraight) downto Max(0, APosition.X - CMaxStraight) do
if (i <> APosition.X) and not FMap[APosition.Y][i].Horizontal.NeedsUpdate then
begin
FMap[APosition.Y][i].Horizontal.NeedsUpdate := True;
if not FMap[APosition.Y][i].Vertical.NeedsUpdate then
AWorkQueue.Enqueue(Point(i, APosition.Y));
end;
end;
procedure TClumsyCrucible.InvalidateVerticalNeighbors(constref APosition: TPoint; constref AWorkQueue: TWorkQueue);
var
i: Integer;
begin
for i := Min(FMap.Count - 1, APosition.Y + CMaxStraight) downto Max(0, APosition.Y - CMaxStraight) do
if (i <> APosition.Y) and not FMap[i][APosition.X].Vertical.NeedsUpdate then
begin
FMap[i][APosition.X].Vertical.NeedsUpdate := True;
if not FMap[i][APosition.X].Horizontal.NeedsUpdate then
AWorkQueue.Enqueue(Point(APosition.X, i));
end;
end;
constructor TClumsyCrucible.Create;
begin
FMap := TNodeArrays.Create;
FMap := TNodeMap.Create;
end;
destructor TClumsyCrucible.Destroy;
@ -114,116 +333,15 @@ begin
end;
procedure TClumsyCrucible.ProcessDataLine(const ALine: string);
var
i: Integer;
nodes: TNodeArray;
begin
FWidth := Length(ALine);
SetLength(nodes, FWidth);
for i := 0 to FWidth - 1 do
begin
nodes[i].LocalHeatLoss := StrToInt(ALine[i + 1]);
nodes[i].Horizontal.IsTraversed := False;
nodes[i].Horizontal.NeedsUpdate := False;
nodes[i].Vertical.IsTraversed := False;
nodes[i].Vertical.NeedsUpdate := False;
end;
FMap.Add(nodes);
FMap.AddNodes(ALine);
end;
procedure TClumsyCrucible.Finish;
var
queue: TWorkQueue;
position: TPoint;
node: TNode;
newMinimum, acc: Cardinal;
i: Integer;
begin
queue := TWorkQueue.Create;
// Initializes work queue with end node.
FMap.Last[FWidth - 1].Horizontal.Minimum := 0;
FMap.Last[FWidth - 1].Horizontal.IsTraversed := True;
FMap.Last[FWidth - 1].Vertical := FMap.Last[FWidth - 1].Horizontal;
position := Point(FWidth - 1, FMap.Count - 1);
InvalidateHorizontalNeighbors(position, queue);
InvalidateVerticalNeighbors(position, queue);
// Processes work queue.
while queue.Count > 0 do
begin
position := queue.Dequeue;
node := FMap[position.Y][position.X];
// Updates horizontal data.
if node.Horizontal.NeedsUpdate then
begin
node.Horizontal.NeedsUpdate := False;
// Checks for better minimum in left direction.
newMinimum := Cardinal.MaxValue;
acc := 0;
for i := position.X - 1 downto Max(0, position.X - CMaxStraight) do
begin
Inc(acc, FMap[position.Y][i].LocalHeatLoss);
if FMap[position.Y][i].Vertical.IsTraversed then
newMinimum := Min(newMinimum, FMap[position.Y][i].Vertical.Minimum + acc);
end;
// Checks for better minimum in right direction.
acc := 0;
for i := position.X + 1 to Min(FWidth - 1, position.X + CMaxStraight) do
begin
Inc(acc, FMap[position.Y][i].LocalHeatLoss);
if FMap[position.Y][i].Vertical.IsTraversed then
newMinimum := Min(newMinimum, FMap[position.Y][i].Vertical.Minimum + acc);
end;
// Updates horizontal minimum and queues update for neighbors.
if not node.Horizontal.IsTraversed or (node.Horizontal.Minimum > newMinimum) then
begin
node.Horizontal.IsTraversed := True;
node.Horizontal.Minimum := newMinimum;
InvalidateVerticalNeighbors(position, queue);
end;
end;
// Updates vertical data.
if node.Vertical.NeedsUpdate then
begin
node.Vertical.NeedsUpdate := False;
// Checks for better minimum in up direction.
newMinimum := Cardinal.MaxValue;
acc := 0;
for i := position.Y - 1 downto Max(0, position.Y - CMaxStraight) do
begin
Inc(acc, FMap[i][position.X].LocalHeatLoss);
if FMap[i][position.X].Horizontal.IsTraversed then
newMinimum := Min(newMinimum, FMap[i][position.X].Horizontal.Minimum + acc);
end;
// Checks for better minimum in down direction.
acc := 0;
for i := position.Y + 1 to Min(FMap.Count - 1, position.Y + CMaxStraight) do
begin
Inc(acc, FMap[i][position.X].LocalHeatLoss);
if FMap[i][position.X].Horizontal.IsTraversed then
newMinimum := Min(newMinimum, FMap[i][position.X].Horizontal.Minimum + acc);
end;
// Updates vertical minimum and queues update for neighbors.
if not node.Vertical.IsTraversed or (node.Vertical.Minimum > newMinimum) then
begin
node.Vertical.IsTraversed := True;
node.Vertical.Minimum := newMinimum;
InvalidateHorizontalNeighbors(position, queue);
end;
end;
FMap[position.Y][position.X] := node;
end;
queue.Free;
node := FMap[0][0];
FPart1 := Min(node.Horizontal.Minimum, node.Vertical.Minimum);
FPart1 := FMap.FindMinimumPathLength(CMinStraight, CMaxStraight);
FMap.Reset;
FPart2 := FMap.FindMinimumPathLength(CUltraMinStraight, CUltraMaxStraight);
end;
function TClumsyCrucible.GetDataFileName: string;

View File

@ -33,6 +33,22 @@ type
function CreateSolver: ISolver; override;
published
procedure TestPart1;
procedure TestPart2;
end;
{ TExample2ClumsyCrucible }
TExample2ClumsyCrucible = class(TClumsyCrucible)
function GetDataFileName: string; override;
end;
{ TClumsyCrucibleExample2TestCase }
TClumsyCrucibleExample2TestCase = class(TExampleEngineBaseTest)
protected
function CreateSolver: ISolver; override;
published
procedure TestPart2;
end;
implementation
@ -49,8 +65,33 @@ begin
AssertEquals(102, FSolver.GetResultPart1);
end;
procedure TClumsyCrucibleExampleTestCase.TestPart2;
begin
AssertEquals(94, FSolver.GetResultPart2);
end;
{ TExample2ClumsyCrucible }
function TExample2ClumsyCrucible.GetDataFileName: string;
begin
Result := 'clumsy_crucible2.txt';
end;
{ TClumsyCrucibleExample2TestCase }
function TClumsyCrucibleExample2TestCase.CreateSolver: ISolver;
begin
Result := TExample2ClumsyCrucible.Create;
end;
procedure TClumsyCrucibleExample2TestCase.TestPart2;
begin
AssertEquals(71, FSolver.GetResultPart2);
end;
initialization
RegisterTest('TClumsyCrucible', TClumsyCrucibleExampleTestCase);
RegisterTest('TClumsyCrucible', TClumsyCrucibleExample2TestCase);
end.