AdventOfCode2023/solvers/UClumsyCrucible.pas

241 lines
7.3 KiB
Plaintext
Raw Normal View History

{
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 <http://www.gnu.org/licenses/>.
}
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<TNodeArray>;
TWorkQueue = specialize TQueue<TPoint>;
{ 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.