Introduction
In Day 15 problem seems to be the hardest that we struggled with so far. It’s not so obvious at first sight, how it should be solved and the input data for the problem is big enough to prevent us from creating brute-force solutions. Let’s see then how can we approach this problem and what are the hardest parts in its implementation.
Solution
When solving the problem, we face two problems:
- Proper graph representation
- Designing algorithm for path finding
In the first part we need to represent properly the graph based on the input data. According to problem description, we can see that our graph may be interpreted as nodes between adjacent cells from the map, where the weights of the edges are the values from cells to which we enter. In this way, we can find the shortest path (with the smallest sum of weights on edges) to get the solution for given problem.
It’s worth noticing that in the second part of the task we wouldn’t need to repeat the structure in graph, but modify the operations on graph representation in proper way. Unfortunately, such an approach would lead us to the less readable code in place of some memory saving. That’s why we decided to keep the whole representation in memory. In this scenario, graph building process was quite harder, as it required calculating all its nodes, but in the actual algorithm we didn’t have to worry about any graph representation.
Path finding algorithm for this problem is a straightforward application of Dijkstra’s algorithm. It can be described in natural way as follows:
Let’s consider two featured nodes $s, d \in N$ from graph $G(N, E)$. We keep the current shortest distance to every node from $s$ in $dist$. So at the beginning $dist(s) = 0$ and for $n \neq s$ we have $dist(n) = \infty$. We consider all nodes from $N$ and in current step we extract the node $u$ with the shortest path to $s$ in current time. Having that, we consider every its neighbour $n$ - we have to check, if current distance from $s$ to $n$ is not smaller than the distance from $s$ to $u$ plus the weight of the edge between $u$ and $n$
In this way we build the shortest path from $s$ to every node of the graph, so at the end we can just return the length of the shortest path to destination node $d$.
To be able to represent the extraction process efficiently, we use the PriorityQueue
which orders
the nodes in it based on the distance of the node to $s$, which is stored in dist
field of queue node QN
.
Day15.kt
|
|
Extra notes
We used some cool Kotlin features to implement the parsing process as well as the path finding algorithm, so let’s take a look to code one more time with details.
We decided to define some infix fun
that is capable of creating nodes of graph. In Kotlin, we can define
this kind of function for any type, the only restriction is the number of parameters of such functions
that has to be equal to 1. It gives us the possibility to design some cool API, as the presented #
for
building graph nodes with 2 coordinates.
In Dijkstra implementation we used the named lambda neigh
and the return@neigh
statement. This approach
was better than traditional continue
in for
loop because adj[u.n]
might have been null, based on the Map<K, V>
API (as would need extra care with ?: emptyList()
). If you’re new to such a syntax, then let’s read the deep dive
into similar problem with crossinline
from Day 6 where this
construct was used without giving extra name to the scope - here we could also write return@forEach
but presented approach is more readable and fancy 😎.