Skip to content

Commit

Permalink
2023-10 (#6)
Browse files Browse the repository at this point in the history
* 2023-10 WIP

* 2023-10 WIP - as finished with 2 stars

* Refactoring

* Improve Dijkstra All

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

---------

Co-authored-by: Juris <[email protected]>
  • Loading branch information
jurisk and jurisk authored Dec 10, 2023
1 parent eeb39dc commit 532d5b1
Show file tree
Hide file tree
Showing 14 changed files with 514 additions and 17 deletions.
5 changes: 5 additions & 0 deletions scala2/src/main/resources/2023/10-test-1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.....
.S-7.
.|.|.
.L-J.
.....
5 changes: 5 additions & 0 deletions scala2/src/main/resources/2023/10-test-2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
..F7.
.FJ|.
SJ.L7
|F--J
LJ...
9 changes: 9 additions & 0 deletions scala2/src/main/resources/2023/10-test-3.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
...........
.S-------7.
.|F-----7|.
.||.....||.
.||.....||.
.|L-7.F-J|.
.|..|.|..|.
.L--J.L--J.
...........
10 changes: 10 additions & 0 deletions scala2/src/main/resources/2023/10-test-4.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.F----7F7F7F7F-7....
.|F--7||||||||FJ....
.||.FJ||||||||L7....
FJL7L7LJLJ||LJ.L-7..
L--J.L7...LJS7F-7L7.
....F-J..F7FJ|L7L7L7
....L7.F7||L7|.L7L7|
.....|FJLJ|FJ|F7|.LJ
....FJL-7.||.||||...
....L---J.LJ.LJLJ...
10 changes: 10 additions & 0 deletions scala2/src/main/resources/2023/10-test-5.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FF7FSF7F7F7F7F7F---7
L|LJ||||||||||||F--J
FL-7LJLJ||||||LJL-77
F--JF--7||LJLJ7F7FJ-
L---JF-JLJ.||-FJLJJ7
|F|F-JF---7F7-L7L|7|
|FFJF7L7F-JF7|JL---7
7-L-JL7||F7|L7F-7F7|
L.L7LFJ|||||FJL7||LJ
L7JLJL-JLJLJL--JLJ.L
140 changes: 140 additions & 0 deletions scala2/src/main/resources/2023/10.txt

Large diffs are not rendered by default.

140 changes: 140 additions & 0 deletions scala2/src/main/scala/jurisk/adventofcode/y2023/Advent10.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package jurisk.adventofcode.y2023

import cats.implicits._
import jurisk.adventofcode.y2023.pipe.Pipe._
import jurisk.adventofcode.y2023.pipe.{CoordsWithDirection, Pipe}
import jurisk.algorithms.pathfinding.{Bfs, Dijkstra}
import jurisk.geometry.Direction2D.{E, N, S, W}
import jurisk.geometry.{Coords2D, Field2D}
import jurisk.utils.CollectionOps.IterableOps
import jurisk.utils.FileInput._
import jurisk.utils.Parsing.StringOps

object Advent10 {
final case class Input(
animalAt: Coords2D,
field: Field2D[Pipe],
) {
def at(coords: Coords2D): Pipe =
field.atOrElse(coords, Empty)
}

object Input {
def parse(s: String): Input = {
val chars: Field2D[Char] = Field2D.parseFromString(s, identity)

val animalAt = chars.filterCoordsByValue(_ == 'S').singleResultUnsafe

val mapping = Map(
'|' -> N_S,
'-' -> E_W,
'L' -> N_E,
'J' -> N_W,
'7' -> S_W,
'F' -> S_E,
'.' -> Empty,
'S' -> Empty,
)

val field: Field2D[Pipe] = Field2D.parseFromString(s, mapping.apply)

// Find a suitable pipe to replace the `S` animal cell
val animalPipe = Pipe.NonEmpty.filter { candidate =>
candidate.connections.forall { direction =>
field
.atOrElse(animalAt + direction, Empty)
.connections
.contains(direction.invert)
}
}.singleElementUnsafe

val updatedField = field.updatedAtUnsafe(animalAt, animalPipe)

Input(animalAt, updatedField)
}
}

def parse(input: String): Input = Input.parse(input)

// Find distance to all nodes we can get to while going on the track, take the maximum
def part1(data: Input): Int =
Dijkstra
.dijkstraAll(
data.animalAt,
(c: Coords2D) => connectedNeighbours(data.field, c).map(x => (x, 1)),
)
.map { case (coord @ _, (parent @ _, distance)) =>
distance
}
.max

def part2(data: Input): Int = {
// All the track coordinates
val trackCoords = Dijkstra
.dijkstraAll(
data.animalAt,
(c: Coords2D) => connectedNeighbours(data.field, c).map(x => (x, 1)),
)
.keySet

// The field with only track cells left, others are Empty
val onlyTrack = data.field.mapByCoordsWithValues { case (c, v) =>
if (trackCoords.contains(c)) v else Empty
}

// We don't know which direction is inside and which is outside for the animal coordinates,
// but we can figure it out for the top left coordinates. This depends on the order in which `allCoords`
// returns coordinates.
val topLeftFullCoord = onlyTrack.allCoords
.find(x => onlyTrack.atOrElse(x, Empty) != Empty)
.getOrElse("Failed to find".fail)
val topLeftFullValue = onlyTrack.atOrElse(topLeftFullCoord, Empty)
val topLeftStartDirection = topLeftFullValue match {
case Pipe.Empty => "Unexpected".fail
case Pipe.N_S => S
case Pipe.E_W => W
case Pipe.N_E => N
case Pipe.N_W => W
case Pipe.S_W => S
case Pipe.S_E => E
}

val start = CoordsWithDirection(
coords = topLeftFullCoord,
direction = topLeftStartDirection,
)

// Which direction was the animal facing on each track segment?
val trackCoordsWithAnimalDirection =
Bfs.bfsReachable[CoordsWithDirection](
start,
x => x.nextOnTrack(data.field) :: Nil,
)

// Which cells were on the right of the track, as the animal was walking around it?
val rightCoordinateSeeds = trackCoordsWithAnimalDirection
.flatMap(x => x.coordsToTheRight(data.field).toSet)
.toSet
.diff(trackCoords)

// Let's flood-fill from the coordinates which we know are on the right side of the track
val floodFilled = rightCoordinateSeeds.flatMap { c =>
Bfs.bfsReachable[Coords2D](
c,
x => data.field.adjacent4(x).toSet.diff(trackCoords).toList,
)
}

floodFilled.size
}

def parseFile(fileName: String): Input =
parse(readFileText(fileName))

def main(args: Array[String]): Unit = {
val realData: Input = parseFile("2023/10.txt")

println(s"Part 1: ${part1(realData)}")
println(s"Part 2: ${part2(realData)}")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package jurisk.adventofcode.y2023.pipe

import jurisk.adventofcode.y2023.pipe.Pipe.Empty
import jurisk.geometry.Direction2D._
import jurisk.geometry.{Coords2D, Direction2D, Field2D, Rotation}
import jurisk.utils.CollectionOps.IterableOps

final case class CoordsWithDirection(
coords: Coords2D,
direction: CardinalDirection2D,
) {
// Next coordinate & direction if we walk along the track
def nextOnTrack(field: Field2D[Pipe]): CoordsWithDirection = {
val nextCoords = coords + direction
val nextSquare = field.atOrElse(nextCoords, Empty)

val nextDirection = nextSquare.connections
.filterNot(_ == direction.invert)
.singleElementUnsafe

CoordsWithDirection(
coords = nextCoords,
direction = nextDirection,
)
}

// Coordinates to the right of these `coords`, if facing in `direction`
def coordsToTheRight(field: Field2D[Pipe]): List[Coords2D] = {
val diffs: List[Direction2D] = field.atOrElse(coords, Empty) match {
case Pipe.Empty => Nil

case Pipe.N_S | Pipe.E_W =>
direction.rotate(Rotation.Right90) :: Nil

case Pipe.N_E =>
direction match {
case Direction2D.E => W :: SW :: S :: Nil
case _ => Nil
}
case Pipe.N_W =>
direction match {
case Direction2D.N => S :: SE :: E :: Nil
case _ => Nil
}
case Pipe.S_W =>
direction match {
case Direction2D.W => N :: NE :: E :: Nil
case _ => Nil
}
case Pipe.S_E =>
direction match {
case Direction2D.S => N :: NW :: W :: Nil
case _ => Nil
}
}

diffs.map(x => coords + x)
}
}
67 changes: 67 additions & 0 deletions scala2/src/main/scala/jurisk/adventofcode/y2023/pipe/Pipe.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package jurisk.adventofcode.y2023.pipe

import jurisk.geometry.Direction2D.{CardinalDirection2D, E, N, S, W}
import jurisk.geometry.{Coords2D, Field2D}

sealed trait Pipe {
def symbol: Char
def connections: Set[CardinalDirection2D]
}

case object Pipe {
def connectedNeighbours(
field: Field2D[Pipe],
coords: Coords2D,
): List[Coords2D] =
field
.atOrElse(coords, Empty)
.connections
.filter { direction =>
field
.atOrElse(coords + direction, Empty)
.connections
.contains(direction.invert)
}
.map { direction =>
coords + direction
}
.toList

val NonEmpty: List[Pipe] = N_S :: E_W :: N_E :: N_W :: S_W :: S_E :: Nil
val All: List[Pipe] = Empty :: NonEmpty

case object Empty extends Pipe {
override def symbol: Char = '░'
override def connections: Set[CardinalDirection2D] = Set.empty
}

case object N_S extends Pipe {
override def symbol: Char = '┃'
override def connections: Set[CardinalDirection2D] = Set(N, S)
}

case object E_W extends Pipe {
override def symbol: Char = '━'
override def connections: Set[CardinalDirection2D] = Set(E, W)
}

case object N_E extends Pipe {
override def symbol: Char = '┗'
override def connections: Set[CardinalDirection2D] = Set(N, E)
}

case object N_W extends Pipe {
override def symbol: Char = '┛'
override def connections: Set[CardinalDirection2D] = Set(N, W)
}

case object S_W extends Pipe {
override def symbol: Char = '┓'
override def connections: Set[CardinalDirection2D] = Set(S, W)
}

case object S_E extends Pipe {
override def symbol: Char = '┏'
override def connections: Set[CardinalDirection2D] = Set(S, E)
}
}
18 changes: 14 additions & 4 deletions scala2/src/main/scala/jurisk/algorithms/pathfinding/Dijkstra.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,22 @@ object Dijkstra {
* @param successors
* List of successors for a given node, along with the cost of moving from
* the given node to this successor.
* @param returnStart
* Whether to return `start` node in the results
* @tparam N
* Node type.
* @tparam C
* Cost type.
* @return
* A map where every reachable node (not including `start`) is associated
* with the optimal parent node and a cost from the start node.
* A map where every reachable node is associated with the optimal parent
* node and a cost from the start node.
*/
// https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm#Pseudocode
// 1 function Dijkstra(Graph, source):
def dijkstraAll[N, C: Numeric: Bounded](
start: N,
successors: N => List[(N, C)],
returnStart: Boolean = true,
): Map[N, (N, C)] = {
val Zero = implicitly[Numeric[C]].zero
val MaxValue = implicitly[Bounded[C]].maxValue
Expand Down Expand Up @@ -77,9 +80,16 @@ object Dijkstra {
}
}

dist.remove(start) // Not returning start
// This flag was added because the callers sometimes expected to find `start` in the list of results
if (!returnStart) {
dist.remove(start)
}

dist.map { case (n, c) =>
val parent = prev(n)
val parent = prev.getOrElse(
n,
start,
) // Note - parent of `start` will be `start`, if returned
(n, (parent, c))
}.toMap
}
Expand Down
2 changes: 2 additions & 0 deletions scala2/src/main/scala/jurisk/geometry/Direction2D.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ object Direction2D {

def toCaret: Char = caretMapping.rightToLeftUnsafe(this)

def invert: CardinalDirection2D = rotate(Rotation.TurnAround)

def rotate(rotation: Rotation): CardinalDirection2D =
(rotation, this) match {
case (NoRotation, _) => this
Expand Down
Loading

0 comments on commit 532d5b1

Please sign in to comment.