✨ Advent of Code — Day 15 <2021 />
DAY 15: CHITON
* Cf. aoc. d. xv
* SSSP = Single Source Shortest Path problem
Today’s challenge is a * SSSP. Given that we are dealing with a weighted graph, we can’t use BFS to solve the problem. We need to use Dijkstra’s algorithm, or Bellman-Ford’s algorithm.
As always, if you want to go straight to the code, you can check it out on our codelicia’s repository.
THE INPUT
The input was very straightforward to parse. We need to convert the String
into a List<List<Int>>
.
class Day15(val input: String) {
val grid = input.split("\n").map { it ->
it.toCharArray().map { it.digitToInt() }
}
}
fun main(): Unit = println(Day15("1163751742\n1381373672\n2136511328\n3694931569\n7463417111\n1319128137\n1359912421\n3125421639\n1293138521\n2311944581").grid)
Now we have a List<List<Int>>
, or in other words, a graph.
PARS I
First, I approached part one with dynamic programming.
import kotlin.math.min
class Day15(val input: String) {
private val grid = input.split("\n").map { it ->
it.toCharArray().map { it.digitToInt() }
}
private val maxRow = grid.lastIndex
private val maxColumn = grid.last().lastIndex
fun part1(): Int {
val dp = Array(maxRow + 1) { IntArray(maxColumn + 1) { Int.MAX_VALUE } }
dp[0][0] = 0
for (i in 0..maxRow) {
for (j in 0..maxColumn) {
if (i > 0) dp[i][j] = min(dp[i][j], dp[i - 1][j] + grid[i][j])
if (j > 0) dp[i][j] = min(dp[i][j], dp[i][j - 1] + grid[i][j])
}
}
return dp[maxRow][maxColumn]
}
}
fun main(): Unit = println(Day15("1163751742\n1381373672\n2136511328\n3694931569\n7463417111\n1319128137\n1359912421\n3125421639\n1293138521\n2311944581").part1())
dp
is a matrix that will store the minimum cost to reach a given position.dp[0][0] = 0
is the base case. The minimum cost to reach the starting position is zero.dp[i][j] = min(dp[i][j], dp[i - 1][j] + grid[i][j])
is the recurrence relation. We can reach the position(i, j)
from the positions(i - 1, j)
and(i, j - 1)
. We need to take the minimum cost between these two positions and add the cost of the current position.- The answer is
dp[maxRow][maxColumn]
, the minimum cost to reach the bottom right position.
Yay! We got the right answer, and our first start 🌟
PARS II
Part II was very difficult for me. Believe you or not, my daily life as a developer doesn’t envolve finding the shortest path between two points in a graph, or inverting a binary tree 👨🏻💻
* Maybe we can, if you pass twice through the graph
I knew that I needed to use Dijkstra’s algorithm, but I didn’t know how to implement it. So I went to the internet and watched a few videos about it. We can’t solve it using * dynamic programming because we need to walk in every direction of the graph, and not only in the right and down directions.
First thing I did was to create a method to enlarge the map, after that, I created a method to find the shortest path
between two points in the graph. The Node
class is a simple class that holds the position of the node and the cost to
reach it. It serves a helper class to the dijkstra
method.
package com.codelicia.advent2021
import java.util.*
import kotlin.math.min
class Day15(val input: String) {
// Split the input string into a 2D grid of integers
private val grid = input.split("\n").map { it ->
it.toCharArray().map { it.digitToInt() }
}
private val maxRow = grid.lastIndex
private val maxColumn = grid.last().lastIndex
fun part1(): Int {
val dp = Array(maxRow + 1) { IntArray(maxColumn + 1) { Int.MAX_VALUE } }
dp[0][0] = 0
for (i in 0..maxRow) {
for (j in 0..maxColumn) {
if (i > 0) dp[i][j] = min(dp[i][j], dp[i - 1][j] + grid[i][j])
if (j > 0) dp[i][j] = min(dp[i][j], dp[i][j - 1] + grid[i][j])
}
}
return dp[maxRow][maxColumn]
}
fun part2(): Int {
val heap: PriorityQueue<Node> = PriorityQueue()
heap.add(Node(0, 0, 0))
val g = enlargeMap().split("\n").map { it ->
it.toCharArray().map { it.digitToInt() }
}
val visited: MutableSet<Pair<Int, Int>> = mutableSetOf()
while (heap.isNotEmpty()) {
val node = heap.poll()
val cost = node.cost
val x = node.x
val y = node.y
if (x == g.lastIndex && y == g.last().lastIndex) {
return cost
}
if (x to y in visited) continue
visited.add(x to y)
for ((nx, ny) in listOf(x to y-1, x to y+1, x-1 to y, x+1 to y)) {
if (nx < 0 || ny < 0 || nx > g.lastIndex || ny > g.last().lastIndex) continue
if (nx to ny in visited) continue
heap.add(Node(cost + g[nx][ny], nx, ny))
}
}
error("Could not find the shortest path")
}
fun Int.inc(i: Int): Int = if (this + i > 9) (this + i) % 9 else this+i
fun enlargeMap(): String {
// pad right
var s : String = ""
for (i in 0..maxRow) {
repeat(5) {
repeatIdx -> s += grid[i].map { it.inc(repeatIdx) }.joinToString("")
}
s += "\n"
}
// pad bottom
repeat(4) { rp ->
var paddedGrid = s.split("\n").map { it -> it.toCharArray().map { it.digitToInt() } }
for (i in 0..maxRow) {
s += paddedGrid[i].map { it.inc(rp+1) }.joinToString("") + "\n"
}
}
return s.trim()
}
private class Node(val cost: Int, val x: Int, val y: Int) : Comparable<Node> {
override fun compareTo(other: Node): Int = cost.compareTo(other.cost)
}
}
- Insert references
- Enlarge map
- Node class
-
Priority Queue
- Dijkstra’s Algorithm - Computerphile