AOC 23 - Day 17 - Clumsy Crucible
Today's problem was fun, even if it involved playing with a 2d grid again. The solution was a simple path-finding algorithm, but I like them so that's ok.
Part 1
Rules
Today's problem is that we have to push crucibles from one point to another, while minimizing the heat loss on the way.
The input is a map of the heat loss which occurs on each tile:
2413432311323
3215453535623
3255245654254
3446585845452
4546657867536
1438598798454
4457876987766
3637877979653
4654967986887
4564679986453
1224686865563
2546548887735
4322674655533
The starting point is the top left tile, and we must reach the bottom right tile, picking the path with the less heat loss.
The constraints are the following:
- We can move at most 3 tiles in the same direction
- We can only turn 90 degrees left or right
We need to find how much heat loss would occur with the optimal path.
Code
The easiest solution to find the path with the least heat loss is to consider the map as a weighted graph, and to implement a Dijkstra's algorithm.
Let's start by implementing a node in this graph:
=
=
=
=
# The position of this node on the map
:
:
# How many steps is remaining before having to turn
:
# The direction of this node on the map
:
return
=
, +=
+=
-= 1
=
= 3
=
= 3
return
return <
You can see that I borrowed a lot from day 16 for the direction and rotation. I added a implementation of __lt__
which will be required for the usage of heapq in the Dijkstra implementation later.
The nodes are handled by the class Graph
, which will contain the map of heat loss, and provide the possible states which are possible from a given node:
: =
=
=
""" Generate each direct possible state, and the associated heat
loss for each state, from a given node.
"""
yield from
=
yield from
=
yield from
""" Generate each state a node can have by moving in a straight line,
and the heat loss induced by the movement.
"""
return
=
= 0
=
return
+=
yield ,
return < 0 or < 0 or >= or >=
You can see this party is a bit messy, I'll try to fix it in part 2. Once we have the node and the graph, we have all we need to implement a dijkstra:
=
= 0
=
=
,
continue
= +
=
return ,
Nothing super fancy here, it's the classic implementation.
Since we only need the total weight of the path, we don't have to bother keeping the path. We stop when we reach the bottom right node, and return the distances and the ending node.
The part one then just consist of calling the dijkstra:
=
=
=
, , return - + 1
I don't know why I needed to remove added one to the result, I just noticed it while playing with the example, but part 1 is solved !
Part 2
Rules
Part 2 is the same as part one, but we can now move 10 tiles in a straight line, and we need to move at least 4 tiles before turning.
Code
This is not too complicated to update. I chose to add minimum_steps
and maximum_steps
variables to the Graph
class.
I then have to update the advance
method:
# ...
=
=
# ...
# Return if no moves remaining
return
=
= 0
# If the node just rotated,
# we need to advance the minimum numbers of steps first
return
+=
yield ,
# We then return each remaining step individually
=
return
+=
yield ,
We then have to change the initialization of the graph:
= 4, 10
, =
=
=
, return
And the part 2 is done !
Optimization
But it's so slow ! The solution takes almost 40 seconds to process the 2 parts:
python main.py 17 37.01s user 0.29s system 99% cpu 37.520 total
Let's see if we can make it faster. First, we start with a quick profiling:
We can see that the call to copy
is taking the most time. Let's have a look at this method again:
# ...
return
To be fast, I used the implementation from the stdlib. Would implementing our own version make it faster ? Let's try with:
# ...
return
This is indeed way faster:
python main.py 17 25.10s user 0.18s system 99% cpu 25.319 total
Why ? I am not sure, but I would say it's because the copy
function from the stdlib has to introspect each field of the object to create a copy, were our implementation is accessing each field directly.
This is still slow, but I think less than 30 seconds is enough for me, so let's call it a day !
All the code is available on github