Skip to content

Commit 5128b52

Browse files
Add 2023 day 23 solution and input (#545)
* Port day 23 solution from stewSquared/advent-of-code * add example input for 2023 day 23 * 2023 day 17 use givens rather than globals * 2023 day 17 refactor for clarity * refactor to part1, part2 --------- Co-authored-by: Jamie Thompson <[email protected]>
1 parent 38bc83f commit 5128b52

File tree

1 file changed

+130
-0
lines changed

1 file changed

+130
-0
lines changed

2023/src/day23.scala

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package day23
2+
// based on solution from https://github.com/stewSquared/adventofcode/blob/src/main/scala/2023/Day23.worksheet.sc
3+
4+
import locations.Directory.currentDir
5+
import inputs.Input.loadFileSync
6+
7+
@main def part1: Unit =
8+
println(s"The solution is ${part1(loadInput())}")
9+
10+
@main def part2: Unit =
11+
println(s"The solution is ${part2(loadInput())}")
12+
13+
def loadInput(): String = loadFileSync(s"$currentDir/../input/day23")
14+
15+
import collection.immutable.BitSet
16+
17+
def part1(input: String): Int =
18+
given maze: Maze = Maze(parseInput(input))
19+
longestDownhillHike
20+
21+
def part2(input: String): Int =
22+
given maze: Maze = Maze(parseInput(input))
23+
longestHike
24+
25+
def parseInput(fileInput: String): Vector[Vector[Char]] = Vector.from:
26+
for line <- fileInput.split("\n")
27+
yield line.toVector
28+
29+
enum Dir:
30+
case N, S, E, W
31+
32+
def turnRight = this match
33+
case Dir.N => E
34+
case Dir.E => S
35+
case Dir.S => W
36+
case Dir.W => N
37+
38+
def turnLeft = this match
39+
case Dir.N => W
40+
case Dir.W => S
41+
case Dir.S => E
42+
case Dir.E => N
43+
44+
case class Point(x: Int, y: Int):
45+
def dist(p: Point) = math.abs(x - p.x) + math.abs(y - p.y)
46+
def adjacent = List(copy(x = x + 1), copy(x = x - 1), copy(y = y + 1), copy(y = y - 1))
47+
48+
def move(dir: Dir) = dir match
49+
case Dir.N => copy(y = y - 1)
50+
case Dir.S => copy(y = y + 1)
51+
case Dir.E => copy(x = x + 1)
52+
case Dir.W => copy(x = x - 1)
53+
54+
case class Maze(grid: Vector[Vector[Char]]):
55+
56+
def apply(p: Point): Char = grid(p.y)(p.x)
57+
58+
val xRange: Range = grid.head.indices
59+
val yRange: Range = grid.indices
60+
61+
def points: Iterator[Point] = for
62+
y <- yRange.iterator
63+
x <- xRange.iterator
64+
yield Point(x, y)
65+
66+
val walkable: Set[Point] = points.filter(p => grid(p.y)(p.x) != '#').toSet
67+
val start: Point = walkable.minBy(_.y)
68+
val end: Point = walkable.maxBy(_.y)
69+
70+
val junctions: Set[Point] = walkable.filter: p =>
71+
Dir.values.map(p.move).count(walkable) > 2
72+
.toSet + start + end
73+
74+
val slopes = Map.from[Point, Dir]:
75+
points.collect:
76+
case p if apply(p) == '^' => p -> Dir.N
77+
case p if apply(p) == 'v' => p -> Dir.S
78+
case p if apply(p) == '>' => p -> Dir.E
79+
case p if apply(p) == '<' => p -> Dir.W
80+
81+
def connectedJunctions(pos: Point)(using maze: Maze) = List.from[(Point, Int)]:
82+
def walk(pos: Point, dir: Dir): Option[Point] =
83+
val p = pos.move(dir)
84+
Option.when(maze.walkable(p) && maze.slopes.get(p).forall(_ == dir))(p)
85+
86+
def search(pos: Point, facing: Dir, dist: Int): Option[(Point, Int)] =
87+
if maze.junctions.contains(pos) then Some(pos, dist) else
88+
val adjacentSearch = for
89+
nextFacing <- LazyList(facing, facing.turnRight, facing.turnLeft)
90+
nextPos <- walk(pos, nextFacing)
91+
yield search(nextPos, nextFacing, dist + 1)
92+
93+
if adjacentSearch.size == 1 then adjacentSearch.head else None
94+
95+
for
96+
d <- Dir.values
97+
p <- walk(pos, d)
98+
junction <- search(p, d, 1)
99+
yield junction
100+
101+
def longestDownhillHike(using maze: Maze): Int =
102+
def search(pos: Point, dist: Int)(using maze: Maze): Int =
103+
if pos == maze.end then dist else
104+
connectedJunctions(pos).foldLeft(0):
105+
case (max, (n, d)) => max.max(search(n, dist + d))
106+
107+
search(maze.start, 0)
108+
109+
def longestHike(using maze: Maze): Int =
110+
type Index = Int
111+
112+
val indexOf: Map[Point, Index] =
113+
maze.junctions.toList.sortBy(_.dist(maze.start)).zipWithIndex.toMap
114+
115+
val adjacent: Map[Index, List[(Index, Int)]] =
116+
maze.junctions.toList.flatMap: p1 =>
117+
connectedJunctions(p1).flatMap: (p2, d) =>
118+
val forward = indexOf(p1) -> (indexOf(p2), d)
119+
val reverse = indexOf(p2) -> (indexOf(p1), d)
120+
List(forward, reverse)
121+
.groupMap(_._1)(_._2)
122+
123+
def search(junction: Index, visited: BitSet, totalDist: Int): Int =
124+
if junction == indexOf(maze.end) then totalDist else
125+
adjacent(junction).foldLeft(0):
126+
case (longest, (nextJunct, dist)) =>
127+
if visited(nextJunct) then longest else
128+
longest.max(search(nextJunct, visited + nextJunct, totalDist + dist))
129+
130+
search(indexOf(maze.start), BitSet.empty, 0)

0 commit comments

Comments
 (0)