diff --git a/solvers/UClumsyCrucible.pas b/solvers/UClumsyCrucible.pas index b359285..8f53ed5 100644 --- a/solvers/UClumsyCrucible.pas +++ b/solvers/UClumsyCrucible.pas @@ -1,6 +1,6 @@ { Solutions to the Advent Of Code. - Copyright (C) 2023 Stefan Müller + Copyright (C) 2024 Stefan Müller This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -22,14 +22,47 @@ unit UClumsyCrucible; interface uses - Classes, SysUtils, USolver; + Classes, SysUtils, Generics.Collections, Math, USolver; type + { TAxisData } + + TAxisData = record + // The current minimum total heat loss to get from this node to the end if the first step is on this axis. + Minimum: Cardinal; + // True, if and only if the minimum has been set, i.e. a path has been found from this node to the end, with the + // first step on this axis. + IsTraversed, + // True. if a close node was updated (traversed) on the other axis, such that this minimum might be outdated. This + // means that if this node is on the work list, NeedsUpdate is True for at least one of the axes. + NeedsUpdate: Boolean; + end; + + { TNode } + + TNode = record + Horizontal, Vertical: TAxisData; + LocalHeatLoss: Byte; + end; + + TNodeArray = array of TNode; + TNodeArrays = specialize TList; + + TWorkQueue = specialize TQueue; + { 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); public + constructor Create; + destructor Destroy; override; procedure ProcessDataLine(const ALine: string); override; procedure Finish; override; function GetDataFileName: string; override; @@ -38,16 +71,159 @@ type implementation +const + CMaxStraight = 3; + { TClumsyCrucible } -procedure TClumsyCrucible.ProcessDataLine(const ALine: string); +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; +end; + +destructor TClumsyCrucible.Destroy; +begin + FMap.Free; + inherited Destroy; +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); 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); end; function TClumsyCrucible.GetDataFileName: string; diff --git a/tests/UClumsyCrucibleTestCases.pas b/tests/UClumsyCrucibleTestCases.pas index 96da098..148c313 100644 --- a/tests/UClumsyCrucibleTestCases.pas +++ b/tests/UClumsyCrucibleTestCases.pas @@ -1,6 +1,6 @@ { Solutions to the Advent Of Code. - Copyright (C) 2023 Stefan Müller + Copyright (C) 2023-2024 Stefan Müller This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -26,16 +26,6 @@ uses type - { TClumsyCrucibleFullDataTestCase } - - TClumsyCrucibleFullDataTestCase = class(TEngineBaseTest) - protected - function CreateSolver: ISolver; override; - published - procedure TestPart1; - procedure TestPart2; - end; - { TClumsyCrucibleExampleTestCase } TClumsyCrucibleExampleTestCase = class(TExampleEngineBaseTest) @@ -43,28 +33,10 @@ type function CreateSolver: ISolver; override; published procedure TestPart1; - procedure TestPart2; end; implementation -{ TClumsyCrucibleFullDataTestCase } - -function TClumsyCrucibleFullDataTestCase.CreateSolver: ISolver; -begin - Result := TClumsyCrucible.Create; -end; - -procedure TClumsyCrucibleFullDataTestCase.TestPart1; -begin - AssertEquals(-1, FSolver.GetResultPart1); -end; - -procedure TClumsyCrucibleFullDataTestCase.TestPart2; -begin - AssertEquals(-1, FSolver.GetResultPart2); -end; - { TClumsyCrucibleExampleTestCase } function TClumsyCrucibleExampleTestCase.CreateSolver: ISolver; @@ -77,14 +49,8 @@ begin AssertEquals(102, FSolver.GetResultPart1); end; -procedure TClumsyCrucibleExampleTestCase.TestPart2; -begin - AssertEquals(-1, FSolver.GetResultPart2); -end; - initialization - //RegisterTest(TClumsyCrucibleFullDataTestCase); - //RegisterTest(TClumsyCrucibleExampleTestCase); + RegisterTest('TClumsyCrucible', TClumsyCrucibleExampleTestCase); end.