166 lines
3.6 KiB
Nim
166 lines
3.6 KiB
Nim
import std/algorithm
|
|
import std/deques
|
|
import std/enumerate
|
|
import std/heapqueue
|
|
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
|
|
import std/times
|
|
|
|
proc dbg[T](v: T): T = (echo(v); v)
|
|
|
|
func empty[T](v: seq[T]): bool = v.len == 0
|
|
|
|
proc split[T](v: seq[T], pred: proc(elt: T): bool {.noSideEffect.}): seq[seq[T]] =
|
|
var current: seq[T]
|
|
for elt in v:
|
|
if pred(elt):
|
|
result.add current
|
|
current = @[]
|
|
else:
|
|
current.add elt
|
|
result.add current
|
|
|
|
func join[T](parts: seq[seq[T]], sep: T): seq[T] =
|
|
if parts.len > 0:
|
|
result &= parts[0]
|
|
|
|
for part in parts[1..^1]:
|
|
result.add sep
|
|
result &= part
|
|
|
|
type Point = tuple
|
|
r: int
|
|
c: int
|
|
|
|
type Map = object
|
|
height: int
|
|
width: int
|
|
grid: Table[Point, int]
|
|
|
|
func parseMap(text: string): Map =
|
|
let lines = text.strip.splitLines()
|
|
result.height = lines.len
|
|
result.width = lines[0].len
|
|
|
|
for (r, line) in enumerate(lines):
|
|
for (c, sym) in enumerate(line):
|
|
result.grid[(r, c)] = sym.int - '0'.int
|
|
|
|
type Direction = enum
|
|
N
|
|
W
|
|
S
|
|
E
|
|
|
|
func cw(d: Direction): Direction =
|
|
case d:
|
|
of N: E
|
|
of E: S
|
|
of S: W
|
|
of W: N
|
|
|
|
func ccw(d: Direction): Direction =
|
|
case d:
|
|
of N: W
|
|
of W: S
|
|
of S: E
|
|
of E: N
|
|
|
|
func move(p: Point, d: Direction, n: int): Point =
|
|
case d:
|
|
of N: (p.r - n, p.c)
|
|
of S: (p.r + n, p.c)
|
|
of W: (p.r, p.c - n)
|
|
of E: (p.r, p.c + n)
|
|
|
|
func inBounds(m: Map, p: Point): bool =
|
|
p.r in 0 ..< m.height and p.c in 0 ..< m.width
|
|
|
|
func loss(m: Map, p: Point): int =
|
|
if m.inBounds(p):
|
|
return m.grid[p]
|
|
|
|
type State = tuple
|
|
pos: Point
|
|
dir: Direction
|
|
|
|
type Node = tuple
|
|
state: State
|
|
cost: int
|
|
|
|
func `<`(a, b: Node): bool = a.cost < b.cost
|
|
|
|
func successors(m: Map, state: State): seq[(State, int)] =
|
|
for steps in 1..3:
|
|
var pos = state.pos
|
|
var loss = 0
|
|
for _ in 1..steps:
|
|
pos = pos.move(state.dir, 1)
|
|
loss += m.loss(pos)
|
|
|
|
result.add ((pos, state.dir.cw), loss)
|
|
result.add ((pos, state.dir.ccw), loss)
|
|
|
|
type Successors = (Map, State) -> seq[(State, int)]
|
|
|
|
proc heatLoss(m: Map, start, goal: Point, successors: Successors): int =
|
|
var queue: HeapQueue[Node]
|
|
var seen: HashSet[State]
|
|
|
|
# Unknown starting direction, so try them all.
|
|
queue.push ((start, N), 0)
|
|
queue.push ((start, S), 0)
|
|
queue.push ((start, E), 0)
|
|
queue.push ((start, W), 0)
|
|
|
|
while queue.len > 0:
|
|
let (state, cost) = queue.pop
|
|
|
|
if not m.inBounds(state.pos): continue
|
|
|
|
if state in seen: continue
|
|
seen.incl state
|
|
|
|
if state.pos == goal:
|
|
return cost
|
|
|
|
for (next, extra) in m.successors(state):
|
|
queue.push (next, cost + extra)
|
|
|
|
proc part1(input: string): int =
|
|
let text = readFile(input)
|
|
let map = parseMap(text)
|
|
map.heatLoss((0, 0), (map.height - 1, map.width - 1), successors)
|
|
|
|
func ultra(m: Map, state: State): seq[(State, int)] =
|
|
for steps in 4..10:
|
|
var pos = state.pos
|
|
var loss = 0
|
|
for _ in 1..steps:
|
|
pos = pos.move(state.dir, 1)
|
|
loss += m.loss(pos)
|
|
|
|
result.add ((pos, state.dir.cw), loss)
|
|
result.add ((pos, state.dir.ccw), loss)
|
|
|
|
proc part2(input: string): int =
|
|
let text = readFile(input)
|
|
let map = parseMap(text)
|
|
map.heatLoss((0, 0), (map.height - 1, map.width - 1), ultra)
|
|
|
|
proc timed[T](f: () -> T): T =
|
|
let start = cpuTime()
|
|
result = f()
|
|
echo "Time :", cpuTime() - start
|
|
|
|
echo timed(proc(): int = part1("input/test.txt"))
|
|
echo timed(proc(): int = part1("input/day17.txt"))
|
|
echo timed(proc(): int = part2("input/test.txt"))
|
|
echo timed(proc(): int = part2("input/day17.txt"))
|