Added solution for "Day 23: A Long Walk", part 2

This commit is contained in:
Stefan Müller 2024-09-22 23:54:45 +02:00
parent b5576c66f1
commit e7285e88b5
3 changed files with 275 additions and 22 deletions

View File

@ -184,6 +184,14 @@ For part 1, if a brick lands on a single supporting brick, that brick below cann
For part 2, given a starting brick, the algorithm makes use of the tracked vertical connections to find a group of bricks supported by it, such that all supports of the bricks in the group are also in the group. This group of bricks would fall if the starting brick was disintegrated, so its size is counted for each possible starting brick. For part 2, given a starting brick, the algorithm makes use of the tracked vertical connections to find a group of bricks supported by it, such that all supports of the bricks in the group are also in the group. This group of bricks would fall if the starting brick was disintegrated, so its size is counted for each possible starting brick.
### Day 23: A Long Walk
:mag_right: Puzzle: <https://adventofcode.com/2023/day/23>, :white_check_mark: Solver: [`ULongWalk.pas`](solvers/ULongWalk.pas)
There is a nice *O(|V| * |E|)* algorithm for the maximum flow in a directed acyclic graph, if a topological ordering of the vertices is know. It's relatively easy to parse the edges ("paths") of the long walk from the input such that a topological ordering results, by adding the vertices ("crossings") only after all in-edges have been found.
For part 2, I believe there is no polynomial algorithm known for the general case, and even with the given restraints I was unable to come up with one. Instead, my solution uses a depth-first search to parse all options in the network. This was feasible for the given input with some smart data structures to limit iterations of the vertex or edge lists, and with shortcuts to determine early if a search branch can be abandoned.
### Day 24: Never Tell Me the Odds ### Day 24: Never Tell Me the Odds
:star: :mag_right: Puzzle: <https://adventofcode.com/2023/day/24>, :white_check_mark: Solver: [`UNeverTellMeTheOdds.pas`](solvers/UNeverTellMeTheOdds.pas) :star: :mag_right: Puzzle: <https://adventofcode.com/2023/day/24>, :white_check_mark: Solver: [`UNeverTellMeTheOdds.pas`](solvers/UNeverTellMeTheOdds.pas)

View File

@ -27,16 +27,21 @@ uses
type type
TCrossing = class; TCrossing = class;
TPathSelectionState = (pssNone, pssIncluded, pssExcluded);
{ TPath } { TPath }
TPath = class TPath = class
private private
FEnd: TCrossing; FStart, FEnd: TCrossing;
FLength: Integer; FLength: Integer;
FSelected: TPathSelectionState;
public public
property StartCrossing: TCrossing read FStart;
property EndCrossing: TCrossing read FEnd; property EndCrossing: TCrossing read FEnd;
property Length: Integer read FLength; property Length: Integer read FLength;
constructor Create(const ALength: Integer; const AEnd: TCrossing); property Selected: TPathSelectionState read FSelected write FSelected;
constructor Create(const ALength: Integer; const AStart, AEnd: TCrossing);
end; end;
TPaths = specialize TObjectList<TPath>; TPaths = specialize TObjectList<TPath>;
@ -55,20 +60,51 @@ type
TCrossing = class TCrossing = class
private private
FPosition: TPoint; FPosition: TPoint;
FOutPaths: TPaths; FOutPaths, FPaths: TPaths;
FDistance: Integer; FDistance: Integer;
FNotExcludedDegree: Integer;
public public
property Position: TPoint read FPosition; property Position: TPoint read FPosition;
property OutPaths: TPaths read FOutPaths; property OutPaths: TPaths read FOutPaths;
property Paths: TPaths read FPaths;
property Distance: Integer read FDistance write FDistance; property Distance: Integer read FDistance write FDistance;
property NotExcludedDegree: Integer read FNotExcludedDegree write FNotExcludedDegree;
function CalcNextPickIndex(const AMinIndex: Integer): Integer;
constructor Create(constref APosition: TPoint); constructor Create(constref APosition: TPoint);
destructor Destroy; override; destructor Destroy; override;
procedure AddOutPath(const AOutPath: TPath); procedure AddOutPath(const AOutPath: TPath);
procedure AddInPath(const AInPath: TPath);
end; end;
TCrossings = specialize TObjectList<TCrossing>; TCrossings = specialize TObjectList<TCrossing>;
TCrossingStack = specialize TStack<TCrossing>; TCrossingStack = specialize TStack<TCrossing>;
TPathChoiceResult = (pcrContinue, pcrTargetReached, pcrTargetUnreachable, pcrNoMinimum);
{ TPathChoice }
TPathChoice = class
private
FPrevious: TPathChoice;
FPickIndex: Integer;
FPick: TPath;
FEndCrossing: TCrossing;
FAutoExcludes: TPaths;
FExcludeCost: Int64;
FIncludeCost: Int64;
public
property PickIndex: Integer read FPickIndex;
property EndCrossing: TCrossing read FEndCrossing;
property IncludeCost: Int64 read FIncludeCost;
function Apply(constref ATargetCrossing: TCrossing; const AExcludeCostLimit: Int64): TPathChoiceResult;
procedure Revert;
constructor Create(const AStartCrossing: TCrossing);
constructor Create(const APickIndex: Integer; const APrevious: TPathChoice = nil);
destructor Destroy; override;
end;
TPathChoiceStack = specialize TStack<TPathChoice>;
{ TLongWalk } { TLongWalk }
TLongWalk = class(TSolver) TLongWalk = class(TSolver)
@ -76,12 +112,15 @@ type
FLines: TStringList; FLines: TStringList;
FPaths: TPaths; FPaths: TPaths;
FCrossings, FWaitingForOtherInPath: TCrossings; FCrossings, FWaitingForOtherInPath: TCrossings;
FStart: TCrossing; FPathLengthSum: Int64;
function GetPosition(constref APoint: TPoint): Char; function GetPosition(constref APoint: TPoint): Char;
procedure ProcessPaths; procedure ProcessPaths;
procedure StepPath(const AStartPositionQueue: TPathStartQueue); procedure StepPath(const AStartPositionQueue: TPathStartQueue);
function FindOrCreateCrossing(constref APosition: TPoint; const AStartPositionQueue: TPathStartQueue): TCrossing; function FindOrCreateCrossing(constref APosition: TPoint; const AStartPositionQueue: TPathStartQueue): TCrossing;
// Treats the graph as directed for part 1.
procedure FindLongestPath; procedure FindLongestPath;
// Treats the graph as undirected for part 2.
procedure FindLongestPathIgnoreSlopes;
public public
constructor Create; constructor Create;
destructor Destroy; override; destructor Destroy; override;
@ -101,30 +140,163 @@ implementation
{ TPath } { TPath }
constructor TPath.Create(const ALength: Integer; const AEnd: TCrossing); constructor TPath.Create(const ALength: Integer; const AStart, AEnd: TCrossing);
begin begin
FLength := ALength; FLength := ALength;
FStart := AStart;
FEnd := AEnd; FEnd := AEnd;
FSelected := pssNone;
end; end;
{ TCrossing } { TCrossing }
function TCrossing.CalcNextPickIndex(const AMinIndex: Integer): Integer;
begin
Result := AMinIndex;
while (Result < FPaths.Count) and (FPaths[Result].Selected <> pssNone) do
Inc(Result);
end;
constructor TCrossing.Create(constref APosition: TPoint); constructor TCrossing.Create(constref APosition: TPoint);
begin begin
FPosition := APosition; FPosition := APosition;
FOutPaths := TPaths.Create(False); FOutPaths := TPaths.Create(False);
FPaths := TPaths.Create(False);
FDistance := 0; FDistance := 0;
FNotExcludedDegree := 0;
end; end;
destructor TCrossing.Destroy; destructor TCrossing.Destroy;
begin begin
FOutPaths.Free; FOutPaths.Free;
FPaths.Free;
inherited Destroy; inherited Destroy;
end; end;
procedure TCrossing.AddOutPath(const AOutPath: TPath); procedure TCrossing.AddOutPath(const AOutPath: TPath);
begin begin
FOutPaths.Add(AOutPath); FOutPaths.Add(AOutPath);
FPaths.Add(AOutPath);
Inc(FNotExcludedDegree);
end;
procedure TCrossing.AddInPath(const AInPath: TPath);
begin
FPaths.Add(AInPath);
Inc(FNotExcludedDegree);
end;
{ TPathChoice }
function TPathChoice.Apply(constref ATargetCrossing: TCrossing; const AExcludeCostLimit: Int64): TPathChoiceResult;
var
path: TPath;
excludeStack: TCrossingStack;
crossing, otherCrossing: TCrossing;
begin
Result := pcrContinue;
// Includes the selected path (edge) and checks whether target has been reached.
FPick.Selected := pssIncluded;
if FEndCrossing = ATargetCrossing then
Result := pcrTargetReached
else if FPrevious <> nil then
begin
// If the target has not been reached, starts at the starting crossing (which is the same as FPRevious.EndCrossing)
// and recursively excludes other connected paths (edges).
excludeStack := TCrossingStack.Create;
excludeStack.Push(FPrevious.EndCrossing);
while excludeStack.Count > 0 do
begin
crossing := excludeStack.Pop;
for path in crossing.Paths do
if path.Selected = pssNone then
begin
// Checks whether the path (edge) to the target crossing has been excluded and if so exits. The input data
// should be such that there is only one such path.
// The last crossing is always an end, never a start of a path (edge).
if path.EndCrossing = ATargetCrossing then
begin
Result := pcrTargetUnreachable;
excludeStack.Free;
Exit;
end
else begin
// Excludes the path (edge).
path.Selected := pssExcluded;
crossing.NotExcludedDegree := crossing.NotExcludedDegree - 1;
FAutoExcludes.Add(path);
FExcludeCost := FExcludeCost + path.Length;
// Checks if this choice is worse than the current best.
if FExcludeCost >= AExcludeCostLimit then
begin
Result := pcrNoMinimum;
excludeStack.Free;
Exit;
end;
// Finds the crossing on the other side, updates it, and possibly pushes it for recursion.
if crossing = path.StartCrossing then
otherCrossing := path.EndCrossing
else
otherCrossing := path.StartCrossing;
otherCrossing.NotExcludedDegree := otherCrossing.NotExcludedDegree - 1;
if otherCrossing.NotExcludedDegree < 2 then
excludeStack.Push(otherCrossing);
end;
end;
end;
excludeStack.Free;
end;
end;
procedure TPathChoice.Revert;
var
path: TPath;
begin
FPick.Selected := pssNone;
for path in FAutoExcludes do begin
path.Selected := pssNone;
path.StartCrossing.NotExcludedDegree := path.StartCrossing.NotExcludedDegree + 1;
path.EndCrossing.NotExcludedDegree := path.EndCrossing.NotExcludedDegree + 1;
end;
end;
constructor TPathChoice.Create(const AStartCrossing: TCrossing);
begin
FPrevious := nil;
FPickIndex := 0;
FPick := AStartCrossing.Paths[FPickIndex];
FEndCrossing := FPick.EndCrossing;
FExcludeCost := 0;
FIncludeCost := FPick.FLength;
FAutoExcludes := TPaths.Create(False);
end;
constructor TPathChoice.Create(const APickIndex: Integer; const APrevious: TPathChoice);
begin
FPrevious := APrevious;
FPickIndex := APickIndex;
FPick := FPrevious.EndCrossing.Paths[FPickIndex];
if FPick.StartCrossing = FPrevious.EndCrossing then
FEndCrossing := FPick.EndCrossing
else
FEndCrossing := FPick.StartCrossing;
FExcludeCost := FPrevious.FExcludeCost;
FIncludeCost := FPrevious.FIncludeCost + FPick.FLength;
FAutoExcludes := TPaths.Create(False);
end;
destructor TPathChoice.Destroy;
begin
FAutoExcludes.Free;
inherited Destroy;
end; end;
{ TLongWalk } { TLongWalk }
@ -136,17 +308,17 @@ end;
procedure TLongWalk.ProcessPaths; procedure TLongWalk.ProcessPaths;
var var
stack: TPathStartQueue; queue: TPathStartQueue;
pathStart: TPathStart; pathStart: TPathStart;
begin begin
stack := TPathStartQueue.Create; queue := TPathStartQueue.Create;
pathStart.Position := FStart.Position; pathStart.Crossing := FCrossings.First;
pathStart.Crossing := FStart; pathStart.Position := FCrossings.First.Position;
pathStart.ReverseDirection := CDirectionUp; pathStart.ReverseDirection := CDirectionUp;
stack.Enqueue(pathStart); queue.Enqueue(pathStart);
while stack.Count > 0 do while queue.Count > 0 do
StepPath(stack); StepPath(queue);
stack.Free; queue.Free;
end; end;
procedure TLongWalk.StepPath(const AStartPositionQueue: TPathStartQueue); procedure TLongWalk.StepPath(const AStartPositionQueue: TPathStartQueue);
@ -161,8 +333,8 @@ var
path: TPath; path: TPath;
begin begin
start := AStartPositionQueue.Dequeue; start := AStartPositionQueue.Dequeue;
len := 1; len := 0;
if start.Crossing <> FStart then if start.Crossing <> FCrossings.First then
Inc(len); Inc(len);
oneMore := False; oneMore := False;
stop := False; stop := False;
@ -190,9 +362,11 @@ begin
until stop; until stop;
crossing := FindOrCreateCrossing(start.Position, AStartPositionQueue); crossing := FindOrCreateCrossing(start.Position, AStartPositionQueue);
path := TPath.Create(len, crossing); path := TPath.Create(len, start.Crossing, crossing);
FPathLengthSum := FPathLengthSum + path.FLength;
FPaths.Add(path); FPaths.Add(path);
start.Crossing.AddOutPath(path); start.Crossing.AddOutPath(path);
crossing.AddInPath(path);
end; end;
// Crossing with multiple (two) entries will only be added to FCrossings once both in-paths have been processed. This // Crossing with multiple (two) entries will only be added to FCrossings once both in-paths have been processed. This
@ -255,6 +429,8 @@ begin
end end
end; end;
// In a directed graph with a topological ordering on the crossings (vertices), the maximum distance can be computed
// simply by traversing the crossings in that order and calculating the maximum locally.
procedure TLongWalk.FindLongestPath; procedure TLongWalk.FindLongestPath;
var var
crossing: TCrossing; crossing: TCrossing;
@ -264,17 +440,82 @@ begin
begin begin
for path in crossing.OutPaths do for path in crossing.OutPaths do
if path.EndCrossing.Distance < crossing.Distance + path.Length then if path.EndCrossing.Distance < crossing.Distance + path.Length then
path.EndCrossing.Distance := crossing.Distance + path.Length; path.EndCrossing.Distance := crossing.Distance + path.Length + 1;
end; end;
FPart1 := FCrossings.Last.Distance; FPart1 := FCrossings.Last.Distance;
end; end;
// For the undirected graph, we are running a DFS for the second to last crossing (vertex) with backtracking to find the
// minimum of excluded crossings and paths.
procedure TLongWalk.FindLongestPathIgnoreSlopes;
var
pickIndex: Integer;
choice: TPathChoice;
stack: TPathChoiceStack;
minExcludeCost, newExcludeCost: Int64;
begin
minExcludeCost := FPathLengthSum + FCrossings.Count - 1 - FPart1;
// Prepares the first pick, which is the only path connected to the first crossing.
stack := TPathChoiceStack.Create;
choice := TPathChoice.Create(FCrossings.First);
choice.Apply(FCrossings.Last, minExcludeCost);
stack.Push(choice);
// Runs a DFS for last crossing with backtracking, trying to find the minimum cost of excluded paths (i.e. edges).
pickIndex := -1;
while stack.Count > 0 do
begin
// Chooses next path.
pickIndex := stack.Peek.EndCrossing.CalcNextPickIndex(pickIndex + 1);
if pickIndex < stack.Peek.EndCrossing.Paths.Count then
begin
choice := TPathChoice.Create(pickIndex, stack.Peek);
case choice.Apply(FCrossings.Last, minExcludeCost) of
// Continues DFS, target has not yet been reached.
pcrContinue: begin
stack.Push(choice);
pickIndex := -1;
Continue;
end;
// Updates minimum and backtracks last choice, after target has been reached.
pcrTargetReached: begin
// Calculates new exclude cost based on path length sum and the choice's include cost. This effectively
// accounts for the "undecided" paths (edges) as well. Note that this does not actually need the choice's
// exclude costs, these are only required for the early exit in TPathChoice.Apply().
newExcludeCost := FCrossings.Count - stack.Count - 2 + FPathLengthSum - choice.IncludeCost;
if minExcludeCost > newExcludeCost then
minExcludeCost := newExcludeCost;
choice.Revert;
choice.Free;
end;
// Backtracks last choice, after target has been excluded or exclude costs ran over the current best.
pcrTargetUnreachable, pcrNoMinimum: begin
choice.Revert;
choice.Free;
end;
end;
end
else begin
choice := stack.Pop;
pickIndex := choice.PickIndex;
choice.Revert;
choice.Free;
end;
end;
stack.Free;
FPart2 := FPathLengthSum - minExcludeCost + FCrossings.Count - 1;
end;
constructor TLongWalk.Create; constructor TLongWalk.Create;
begin begin
FLines := TStringList.Create; FLines := TStringList.Create;
FPaths := TPaths.Create; FPaths := TPaths.Create;
FCrossings := TCrossings.Create; FCrossings := TCrossings.Create;
FWaitingForOtherInPath := TCrossings.Create(False); FWaitingForOtherInPath := TCrossings.Create(False);
FPathLengthSum := 0;
end; end;
destructor TLongWalk.Destroy; destructor TLongWalk.Destroy;
@ -289,10 +530,7 @@ end;
procedure TLongWalk.ProcessDataLine(const ALine: string); procedure TLongWalk.ProcessDataLine(const ALine: string);
begin begin
if FLines.Count = 0 then if FLines.Count = 0 then
begin FCrossings.Add(TCrossing.Create(Point(ALine.IndexOf(CPathChar) + 1, 0)));
FStart := TCrossing.Create(Point(ALine.IndexOf(CPathChar) + 1, 0));
FCrossings.Add(FStart);
end;
FLines.Add(ALine); FLines.Add(ALine);
end; end;
@ -300,6 +538,7 @@ procedure TLongWalk.Finish;
begin begin
ProcessPaths; ProcessPaths;
FindLongestPath; FindLongestPath;
FindLongestPathIgnoreSlopes;
end; end;
function TLongWalk.GetDataFileName: string; function TLongWalk.GetDataFileName: string;

View File

@ -1,6 +1,6 @@
{ {
Solutions to the Advent Of Code. 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 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 the terms of the GNU General Public License as published by the Free Software
@ -33,6 +33,7 @@ type
function CreateSolver: ISolver; override; function CreateSolver: ISolver; override;
published published
procedure TestPart1; procedure TestPart1;
procedure TestPart2;
end; end;
implementation implementation
@ -49,6 +50,11 @@ begin
AssertEquals(94, FSolver.GetResultPart1); AssertEquals(94, FSolver.GetResultPart1);
end; end;
procedure TLongWalkExampleTestCase.TestPart2;
begin
AssertEquals(154, FSolver.GetResultPart2);
end;
initialization initialization
RegisterTest('TLongWalk', TLongWalkExampleTestCase); RegisterTest('TLongWalk', TLongWalkExampleTestCase);