{ Solutions to the Advent Of Code. 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 Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . } unit UClumsyCrucible; {$mode ObjFPC}{$H+} interface uses 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; function GetPuzzleName: string; override; end; implementation const CMaxStraight = 3; { 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; 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; begin Result := 'clumsy_crucible.txt'; end; function TClumsyCrucible.GetPuzzleName: string; begin Result := 'Day 17: Clumsy Crucible'; end; end.