-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
For some reason, this solution works on my real input, but does not give the expected result for the two sample inputs.
- Loading branch information
1 parent
bef80f2
commit be2e0fa
Showing
5 changed files
with
291 additions
and
235 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,12 @@ | ||
import 'package:aoc_2024/lib.dart'; | ||
import 'package:aoc_2024/day16/part_1.dart' as part1; | ||
import 'package:aoc_2024/day16/part_2.dart' as part2; | ||
|
||
Future<void> main(List<String> arguments) async { | ||
await runDay( | ||
day: Day.day16, | ||
part1: part1.calculate, | ||
part2: (_) => Future.value(0), | ||
part2: part2.calculate, | ||
runReal: true, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,243 +1,16 @@ | ||
import 'dart:io'; | ||
import 'dart:math'; | ||
|
||
import 'package:aoc_2024/lib.dart'; | ||
|
||
import 'paths.dart'; | ||
import 'shared.dart'; | ||
|
||
/// Represents a step in the maze. | ||
enum Step { | ||
straight, | ||
turn; | ||
} | ||
|
||
/// Represents the heading of a reindeer in the maze. | ||
enum Heading { | ||
up, | ||
right, | ||
down, | ||
left; | ||
|
||
/// Calculates the new point based on a given point and this heading. | ||
Point<int> move(Point<int> point) => switch (this) { | ||
up => Point(point.x, point.y - 1), | ||
down => Point(point.x, point.y + 1), | ||
left => Point(point.x - 1, point.y), | ||
right => Point(point.x + 1, point.y) | ||
}; | ||
|
||
/// Returns a new heading representing a clockwise 90 degree rotation. | ||
Heading rotateClockwise() { | ||
var nextIndex = index + 1; | ||
if (nextIndex >= Heading.values.length) { | ||
nextIndex = 0; | ||
} | ||
return Heading.values[nextIndex]; | ||
} | ||
|
||
/// Returns a new heading representing a counter-clockwise 90 degree rotation. | ||
Heading rotateCounterclockwise() { | ||
var nextIndex = index - 1; | ||
if (nextIndex < 0) { | ||
nextIndex = Heading.values.length - 1; | ||
} | ||
return Heading.values[nextIndex]; | ||
} | ||
} | ||
|
||
/// Represents a location and heading of a reindeer in the maze. | ||
typedef ReindeerLocation = ({Heading heading, Point<int> point}); | ||
|
||
/// Represents a potential next step along a path. | ||
typedef NextStep = ({Step step, ReindeerLocation location}); | ||
|
||
/// Represents a candidate for the best path in the maze. | ||
final class CandidatePath implements Comparable<CandidatePath> { | ||
/// Static counter the increments for every created candidate path. | ||
/// This is used for a simple comparison during equality checking. | ||
static int _pathCounter = 0; | ||
|
||
/// Set of all points visited in this path so far. | ||
final Set<Point<int>> visited; | ||
|
||
/// List of steps taken to get to the current position. | ||
final List<Step> steps; | ||
|
||
/// Current position and heading of the reindeer along this path. | ||
ReindeerLocation current; | ||
|
||
/// Unique index of the path | ||
final int _index; | ||
|
||
/// Current score of the path. This is updated during each move, | ||
/// rather than being calculated on the fly, since the score is used | ||
/// for sorting the candidate list. | ||
int _score; | ||
|
||
CandidatePath(this.visited, this.steps, this.current, {score = 0}) | ||
: _index = _pathCounter++, | ||
_score = score; | ||
|
||
/// Creates a new candidate from an existing candidate, by copying its | ||
/// list of visited points and steps. | ||
factory CandidatePath.fromCandidate(CandidatePath other) { | ||
return CandidatePath( | ||
{...other.visited}, | ||
[...other.steps], | ||
other.current, | ||
score: other._score, | ||
); | ||
} | ||
|
||
/// Returns the current score of this path. | ||
int get score => _score; | ||
|
||
/// Incorporates the given [nextStep] along this path, including updating | ||
/// the path score. | ||
void step(NextStep nextStep) { | ||
// Track that the next position has been visited, and is the new current. | ||
current = nextStep.location; | ||
visited.add(nextStep.location.point); | ||
|
||
switch (nextStep.step) { | ||
case Step.straight: | ||
steps.add(nextStep.step); | ||
_score += 1; | ||
case Step.turn: | ||
// Any turn is really two steps: a turn, followed by moving straight | ||
// into the new location. | ||
steps.add(nextStep.step); | ||
steps.add(Step.straight); | ||
// Add 1001 to the score: 1000 for the turn, 1 for the straight move. | ||
_score += 1001; | ||
} | ||
} | ||
|
||
@override | ||
int get hashCode => _index; | ||
|
||
@override | ||
bool operator ==(Object other) => | ||
(other is CandidatePath) ? _index == other._index : false; | ||
|
||
@override | ||
int compareTo(CandidatePath other) => score.compareTo(other.score); | ||
} | ||
|
||
/// --- Day 16: Reindeer Maze --- | ||
/// | ||
/// TBD | ||
/// Reindeer Maze during the Reindeer Olympics! | ||
/// | ||
/// Given a maze layout, and a cost to move through the maze (1 point for | ||
/// going straight, 1000 points for a turn), compute the lowest cost path | ||
/// from the start position to the end position. Return that cost. | ||
Future<int> calculate(File file) async { | ||
final maze = await loadData(file); | ||
|
||
// Prime the list of candidate paths with the starting point. | ||
final paths = [ | ||
CandidatePath({maze.start}, [], (heading: Heading.right, point: maze.start)) | ||
]; | ||
|
||
// For each point encountered, store the lowest score so far. | ||
// If we ever have a path that reaches a point with a higher score, | ||
// that path can be discarded. | ||
Map<Point<int>, int> lowestPointScores = {}; | ||
|
||
mainController: | ||
while (paths.isNotEmpty) { | ||
// Keep track of any paths we encounter that are no longer relevant. | ||
List<CandidatePath> pathsToPrune = []; | ||
|
||
// In this iteration, progress each of the lowest score paths by one step. | ||
// This is a slight optimization, as it avoids multiple expensive sorts in | ||
// the case that there are many paths with the lowest score so far. | ||
int lowestScore = paths[0].score; | ||
final currIterationPaths = <CandidatePath>[]; | ||
int i = 0; | ||
while (i < paths.length && paths[i].score == lowestScore) { | ||
currIterationPaths.add(paths[i]); | ||
i++; | ||
} | ||
|
||
for (final path in currIterationPaths) { | ||
// We are iterating along the lowest score paths. If this path has already | ||
// reached the end, the means we are at the end! Break out of the loop. | ||
if (path.current.point == maze.end) { | ||
break mainController; | ||
} | ||
|
||
// Determine whether we have already reached the path's current point | ||
// at a cheaper cost. If so, this path cannot be the best path, so | ||
// mark it to be discarded. | ||
// | ||
// If the score is a tie, keep both paths. | ||
final lowestScore = lowestPointScores[path.current.point] ?? maxInt; | ||
if (lowestScore < path.score) { | ||
pathsToPrune.add(path); | ||
} else if (lowestScore > path.score) { | ||
lowestPointScores[path.current.point] = path.score; | ||
} | ||
|
||
// Find all of the possible next steps along the current path. | ||
final nextSteps = | ||
_getValidStepsFromPoint(maze, path.current, path.visited); | ||
if (nextSteps.isNotEmpty) { | ||
// For any additional valid steps, branch off a new candidate path. | ||
for (int i = 1; i < nextSteps.length; i++) { | ||
final newCandidate = CandidatePath.fromCandidate(path); | ||
newCandidate.step(nextSteps[i]); | ||
paths.add(newCandidate); | ||
} | ||
|
||
// Use the first step option on the current candidate path. | ||
// This is done after adding the new candidate paths, since the logic | ||
// above copies this path's list of steps/visited. | ||
path.step(nextSteps[0]); | ||
} else { | ||
// No additional steps along this path, so prune it. A path that already | ||
// reached the end would have been checked earlier. | ||
pathsToPrune.add(path); | ||
} | ||
} | ||
|
||
// Remove paths that are no longer potential best paths. | ||
for (final pathToPrune in pathsToPrune) { | ||
paths.remove(pathToPrune); | ||
} | ||
|
||
// Sort the paths. This is necessary, over using some sort of SortedList | ||
// data structure, because the scores in the CandidatePaths change | ||
// over time. | ||
paths.sort(); | ||
} | ||
|
||
return paths[0].score; | ||
} | ||
|
||
/// Determines all of the valid next steps from a given path. | ||
/// | ||
/// Rather than accepting a [CandidatePath], this function just accepts the | ||
/// reindeer position and a set of visited points. | ||
/// | ||
/// In reality, this function should never return more than 3 points (since | ||
/// only moves in cardinal directions are allowed, and one of those 4 directions | ||
/// would have already been visited). | ||
List<NextStep> _getValidStepsFromPoint( | ||
Maze maze, ReindeerLocation current, Set<Point<int>> visited) { | ||
// Given the only potential moves (straight, clockwise, counterclockwise) | ||
return [ | ||
(current.heading, Step.straight), | ||
(current.heading.rotateClockwise(), Step.turn), | ||
(current.heading.rotateCounterclockwise(), Step.turn), | ||
] | ||
// Generate the corresponding steps. | ||
.map((h) => ( | ||
step: h.$2, | ||
location: (heading: h.$1, point: h.$1.move(current.point)) | ||
)) | ||
// Filter out any step that would visit a visited point. | ||
.where((ns) => !visited.contains(ns.location.point)) | ||
// Limit to steps that would land on empty spaces or the end. | ||
.where((ns) => | ||
maze.map[ns.location.point] == Location.empty || | ||
maze.map[ns.location.point] == Location.end) | ||
// Convert to a list (since it will later be indexed). | ||
.toList(); | ||
return findBestPath(maze).score; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import 'dart:io'; | ||
import 'dart:math'; | ||
|
||
import 'shared.dart'; | ||
import 'paths.dart'; | ||
|
||
/// Continuing from Part 1, determine the number of unique points that are | ||
/// present along at least one lowest cost path (there may be multiple paths | ||
/// with the same cost). | ||
/// | ||
/// Return the number of unique points. | ||
Future<int> calculate(File file) async { | ||
final maze = await loadData(file); | ||
|
||
final uniquePoints = | ||
findBestPaths(maze).fold(<Point<int>>{}, (v, e) => {...v, ...e.visited}); | ||
return uniquePoints.length; | ||
} |
Oops, something went wrong.