Ah, a proper coding puzzle again after the last couple of days of drudge-work.
Firstly a data model.
data classPos(valx:Int,valy:Int){valup:Posget()=Pos(x,y-1)valdown:Posget()=Pos(x,y+1)valleft:Posget()=Pos(x-1,y)valright:Posget()=Pos(x+1,y)funneighbours():Set<Pos>=setOf(up,down,left,right)}data classModel(valdepth:Int,valtarget:Pos)
Some might suggest the input data format is so simple you can just use string splits and regular expressions. But if I want to make one point in this Advent Of Code it's that parser combinators always result in simpler, better code.
There's one simple insight to part 1, which is that the calculated value for each position depends on the positions above and to the left. So the whole grid can be calculated in a right-then-down or down-then-right order. Let's do the whole thing in an object initialiser:
data classCave(valmodel:Model,vallimit:Pos=model.target){valgrid=makeGrid(limit.x+1,limit.y+1,Square(0,0))init{(0..limit.y).forEach{y->(0..limit.x).forEach{x->valp=Pos(x,y)valgeologicIndex=when{p==model.target->0y==0->x*16807Lx==0->y*48271Lelse->grid[p.left].erosionLevel*grid[p.up].erosionLevel}valerosionLevel=(geologicIndex+model.depth)%20183Lgrid[p]=Square(geologicIndex,erosionLevel)}}}}
The descriptions of erosion level, region type and risk level could all be collapsed since there is a 1:1 relationship. But in my experience of building software, if the customer felt the need to describe these things separately, then model them independently. It's lower cost to have each representation in the code and live with the simple mappings than have to mentally remember the equivalences. And it pays off immensely when the requirements change subtly in the future.
Part 1 is answered simply by computing the sum risk level for the entire cave.
funpart1(input:Model):Int=Cave(input).riskLevel()
Part 2
A meaty part 2 today, which introduces a whole new algorithmic requirement. We have to find the shortest path from the start to the trapped friend. Like Day 15 this can be solved with an application of Dijkstra's algorithm. The difference here is that the cost of moving from region to region varies depending on the equipped tool, and indeed in some cases transitions are not possible due to incompatible tools.
First we'll model the tools, the mappings to region types:
Dijkstra's algorithm needs a "current best" value for each position in the graph. This is a product of time and equipped tool. We'll also make some helpers for deriving new states:
data classState(valtime:Int,valtool:Tool)funState.move(p:Pos)=State(time+1,tool)funState.changeTool(t:Tool)=State(time+7,t)
The Dijkstra algorithm follows the same basic structure as before:
Start with all positions unvisited and the current position at the start
While we haven't visited the end position:
Calculate the cost of moving to all the neighbours of the current position
If this is less than the current cost for that neighbour, update it
Move to the unvisited position with the current least cost
The minimum cost is left in the end position
funCave.dijkstra(start:Pos,end:Pos):State{valunvisited=grid.positions().toMutableSet()valnoPath=State(Int.MAX_VALUE,Tool.NEITHER)valstates=makeGrid(limit.x+1,limit.y+1,noPath)funvalid(p:Pos)=unvisited.contains(p)&&0<=p.x&&p.x<=limit.x&&0<=p.y&&p.y<=limit.ystates[start]=State(0,Tool.TORCH)while(unvisited.contains(end)){valminTime=unvisited.map{p->states[p].time}.min()unvisited.filter{p->states[p].time==minTime}.forEach{current->valavailableTools=grid[current].availableTools()valstate=states[current]current.neighbours().filter(::valid).forEach{neighbour->// we'll come to updating neighbours in a bit}unvisited.remove(current)}}returnstates[end]}
The difference today is that the cost of moving to a neighbour position is considerably more complex and must take the transition of tools into account. Firstly, we must match the tools available at the current position with the tools available at the neighbour position. Then:
If we can move to the neighbour without changing tool the cost is 1 minute.
Or if we can move to the neighbour after changing tool the cost is 7 minutes to change tool then 1 minute to move.
Or if no tool change is possible to reach the neighbour, the move is impossible.
The final aspect of part 2 is that the optimal route may extend beyond the target position. We have no idea how far, but changing tools takes eight times longer than just moving, so the furthest beyond the direct route to the target we might go without changing tools is approximately eight times more. Fudging it a bit and waiting a good old time for the answer:
Sadly this doesn't get me an answer that Advent of Code is happy with. I am seeing a longer path for scale=4 than scale=3, which is strange, and points to the path being dependent on the order that the positions are visited rather than the intrinsic properties of the positions. I'll keep working on this one.
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
Ah, a proper coding puzzle again after the last couple of days of drudge-work.
Firstly a data model.
Some might suggest the input data format is so simple you can just use string splits and regular expressions. But if I want to make one point in this Advent Of Code it's that parser combinators always result in simpler, better code.
Previous solutions with 2D arrays have resulted in slightly messy code. Let's factor out the 2D array stuff.
There's one simple insight to part 1, which is that the calculated value for each position depends on the positions above and to the left. So the whole grid can be calculated in a right-then-down or down-then-right order. Let's do the whole thing in an object initialiser:
The descriptions of erosion level, region type and risk level could all be collapsed since there is a 1:1 relationship. But in my experience of building software, if the customer felt the need to describe these things separately, then model them independently. It's lower cost to have each representation in the code and live with the simple mappings than have to mentally remember the equivalences. And it pays off immensely when the requirements change subtly in the future.
Part 1
Part 1 is answered simply by computing the sum risk level for the entire cave.
Part 2
A meaty part 2 today, which introduces a whole new algorithmic requirement. We have to find the shortest path from the start to the trapped friend. Like Day 15 this can be solved with an application of Dijkstra's algorithm. The difference here is that the cost of moving from region to region varies depending on the equipped tool, and indeed in some cases transitions are not possible due to incompatible tools.
First we'll model the tools, the mappings to region types:
Dijkstra's algorithm needs a "current best" value for each position in the graph. This is a product of time and equipped tool. We'll also make some helpers for deriving new states:
The Dijkstra algorithm follows the same basic structure as before:
The difference today is that the cost of moving to a neighbour position is considerably more complex and must take the transition of tools into account. Firstly, we must match the tools available at the current position with the tools available at the neighbour position. Then:
The final aspect of part 2 is that the optimal route may extend beyond the target position. We have no idea how far, but changing tools takes eight times longer than just moving, so the furthest beyond the direct route to the target we might go without changing tools is approximately eight times more. Fudging it a bit and waiting a good old time for the answer:
Sadly this doesn't get me an answer that Advent of Code is happy with. I am seeing a longer path for scale=4 than scale=3, which is strange, and points to the path being dependent on the order that the positions are visited rather than the intrinsic properties of the positions. I'll keep working on this one.