From 7b33e8b40629a7f29c1ade34f98677fa113eb0d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Tue, 25 Jun 2024 22:22:52 +0200 Subject: [PATCH] Added solution for "Day 17: Clumsy Crucible", part 2 --- solvers/UClumsyCrucible.pas | 396 +++++++++++++++++++---------- tests/UClumsyCrucibleTestCases.pas | 41 +++ 2 files changed, 298 insertions(+), 139 deletions(-) diff --git a/solvers/UClumsyCrucible.pas b/solvers/UClumsyCrucible.pas index 8f53ed5..eb34d32 100644 --- a/solvers/UClumsyCrucible.pas +++ b/solvers/UClumsyCrucible.pas @@ -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; TWorkQueue = specialize TQueue; + { 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; diff --git a/tests/UClumsyCrucibleTestCases.pas b/tests/UClumsyCrucibleTestCases.pas index 148c313..42133dd 100644 --- a/tests/UClumsyCrucibleTestCases.pas +++ b/tests/UClumsyCrucibleTestCases.pas @@ -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.