diff --git a/2023/day10.nim b/2023/day10.nim index 3ada644..ff1bb20 100644 --- a/2023/day10.nim +++ b/2023/day10.nim @@ -2,12 +2,14 @@ import std/algorithm import std/deques import std/enumerate import std/math +import std/options import std/sequtils import std/sets import std/strscans import std/strutils import std/sugar import std/tables +import std/terminal proc dbg[T](v: T): T = (echo(v); v) @@ -33,8 +35,11 @@ func parseSketch(text: string): Sketch = if sym == 'S': result.start = (r, c) +func inBounds(s: Sketch, p: Point): bool = + p.r in 0 ..< s.rows and p.c in 0 ..< s.cols + func pipeAt(s: Sketch, p: Point): char = - if p.r in 0 ..< s.rows and p.c in 0 ..< s.cols: + if s.inBounds(p): s.grid[p] else: '.' @@ -65,6 +70,8 @@ func neighbors(s: Sketch, p: Point): seq[Point] = (0, +1, E, W), # Right E-W ]: let n = (p.r + dr, p.c + dc) + if not s.inBounds(n): + continue if s.hasConn(p, pDir) and s.hasConn(n, nDir): result.add(n) @@ -93,7 +100,190 @@ proc part1(): int = let text = readFile("input/day10.txt") let sketch = parseSketch(text) - for d in values(sketch.reachable): + let reachable = sketch.reachable + for d in values(reachable): result = max(result, d) +proc path(s: Sketch): seq[Point] = + var queue: Deque[seq[Point]] + + queue.addLast @[s.start] + + while queue.len > 0: + let path = queue.popFirst + let point = path[^1] + + if path.len > 1 and s.pipeAt(point) == 'S': + return path + + for n in s.neighbors(point): + if path.len > 1 and path[^2] == n: + continue + queue.addLast(path & @[n]) + +func inBoundsPadding(s: Sketch, p: Point): bool = + p.r in -1 .. s.rows and p.c in -1 .. s.cols + +func neighborsFill(s: Sketch, p: Point): seq[Point] = + for (dr, dc) in [ + (-1, 0), # Up + (+1, 0), # Down + (0, -1), # Left + (0, +1), # Right + ]: + let n = (p.r + dr, p.c + dc) + if s.inBoundsPadding(n): + result.add n + +proc floodFill(s: Sketch, start: Point, path: HashSet[Point]): HashSet[Point] = + var queue: Deque[Point] + + queue.addLast start + + while queue.len > 0: + let state = queue.popFirst + + if state in result: + continue + result.incl state + + for n in s.neighborsFill(state): + if n in path: + continue + queue.addLast n + +func cells(s: Sketch): HashSet[Point] = + for r in 0 ..< s.rows: + for c in 0 ..< s.cols: + result.incl (r, c) + +proc heading(a: Point, b: Point): Direction = + let dr = b.r - a.r + let dc = b.c - a.c + assert(abs(dr) + abs(dc) == 1) + + if dr == -1: return N + if dr == +1: return S + if dc == -1: return W + if dc == +1: return E + assert(false) + +func north(p: Point): Point = (p.r - 1, p.c) +func south(p: Point): Point = (p.r + 1, p.c) +func east(p: Point): Point = (p.r , p.c + 1) +func west(p: Point): Point = (p.r , p.c - 1) + +func northwest(p: Point): Point = (p.r - 1, p.c - 1) +func northeast(p: Point): Point = (p.r - 1, p.c + 1) +func southwest(p: Point): Point = (p.r + 1, p.c - 1) +func southeast(p: Point): Point = (p.r + 1, p.c + 1) + +func sides3(p: Point, h: Direction): (seq[Point], seq[Point]) = + case h: + of N: ( + @[p.northwest, p.west, p.southwest], + @[p.northeast, p.east, p.southeast], + ) + of S: ( + @[p.southeast, p.east, p.northeast], + @[p.southwest, p.west, p.northwest], + ) + of E: ( + @[p.northeast, p.north, p.northwest], + @[p.southeast, p.south, p.southwest], + ) + of W: ( + @[p.southwest, p.south, p.southeast], + @[p.northwest, p.north, p.northeast], + ) + +func sides2(p: Point, h: Direction): (seq[Point], seq[Point]) = + let (l, r) = sides3(p, h) + (l[0..^2], r[0..^2]) + +proc walkPath(s: Sketch, path: seq[Point]): (HashSet[Point], HashSet[Point]) = + for (a, b) in zip(path[0..^2], path[1..^1]): + let h = a.heading(b) + + let (left, right) = sides2(a, h) + for p in left: + result[0].incl p + for p in right: + result[1].incl p + +type Side = enum + Left + Right + +func whichSide(p: Point, left: HashSet[Point], right: HashSet[Point]): Option[Side] = + if p in left and p in right: + return none(Side) + if p in left: + return some(Left) + if p in right: + return some(Right) + return none(Side) + +proc part2(): int = + let text = readFile("input/day10.txt") + let sketch = parseSketch(text) + + let path = sketch.path + assert sketch.pipeAt(path[0]) == 'S' + assert sketch.pipeAt(path[^1]) == 'S' + + let (left, right) = sketch.walkPath(path) + + var candidates = sketch.cells + + let pathSet = path.toHashSet + for p in pathSet: + candidates.excl p + + var foundOutsideSide = false + var outside: Side + for p in sketch.floodFill((-1, -1), pathSet): + candidates.excl p + + if p in pathSet: + continue + + # Marked both ways, inconclusve? + if p in left and p in right: + continue + + if p in left: + if foundOutsideSide: + assert(outside == Left) + foundOutsideSide = true + outside = Left + elif p in right: + if foundOutsideSide: + assert(outside == Right) + foundOutsideSide = true + outside = Right + + var inside: HashSet[Point] + for r in 0 ..< sketch.rows: + for c in 0 ..< sketch.cols: + let p = (r, c) + + # Already known to be outside or on path + if p notin candidates: + continue + + let s = whichSide(p, left, right) + if s.isSome and s.get == outside: + for pOut in sketch.floodFill(p, pathSet): + candidates.excl pOut + elif s.isSome and s.get != outside: + for pIn in sketch.floodFill(p, pathSet): + inside.incl pIn + else: + discard "Inconclusive! I hope this doesn't matter!" + + assert(candidates.len == inside.len) + inside.len + echo part1() +echo part2()