120 lines
3.8 KiB
Smalltalk
120 lines
3.8 KiB
Smalltalk
Object subclass: Racetrack [
|
|
| grid distToEnd start end trackLen allCheats tree |
|
|
rows: r
|
|
[ grid := Grid new rows: r.
|
|
self initTrack ]
|
|
|
|
initTrack
|
|
[ | pos dir step posnList |
|
|
"Find the start, end, and count the path tiles..."
|
|
trackLen := 1.
|
|
grid allPosnsDo: [
|
|
:p | | c | c := grid at: p.
|
|
(c = $S or: [c = $E]) ifTrue: [
|
|
grid at: p put: $..
|
|
c = $S ifTrue: [start := p].
|
|
c = $E ifTrue: [end := p].
|
|
].
|
|
c = $. ifTrue: [ trackLen := trackLen + 1 ]].
|
|
distToEnd := Grid width: grid width
|
|
height: grid height
|
|
initWith: [:p | nil].
|
|
|
|
"Trace the path, obtaining the distances to the end..."
|
|
pos := start.
|
|
dir := {Posn up. Posn down. Posn left. Posn right} findFirstElt: [
|
|
:dir | self isEmpty: start + dir ].
|
|
step := 0.
|
|
[ pos = end ] whileFalse: [
|
|
distToEnd at: pos put: trackLen - step.
|
|
dir := {dir. dir rotateCw. dir rotateCcw} findFirstElt: [
|
|
:dir | self isEmpty: pos + dir ].
|
|
pos := pos + dir.
|
|
step := step + 1.
|
|
].
|
|
distToEnd at: end put: 0.
|
|
|
|
"Build a k-d tree to answer range queries
|
|
over the {distToEnd . x . y} dimensions."
|
|
posnList := OrderedCollection new.
|
|
distToEnd allPosnsDo: [
|
|
:p | (distToEnd at: p) ifNotNil: [
|
|
:dist | posnList add: {dist. p x. p y}]].
|
|
tree := KDTree fromPoints: posnList. ]
|
|
|
|
isEmpty: pos [ ^(grid at: pos) = $. ]
|
|
|
|
countCheatsWithTime: time
|
|
[ | total |
|
|
total := 0.
|
|
grid allPosnsDo: [
|
|
:pos | total := total + (self countCheatsFrom: pos withTime: time) ].
|
|
^total ]
|
|
|
|
countCheatsFrom: pos withTime: time
|
|
[ | dist count |
|
|
dist := distToEnd at: pos.
|
|
dist ifNil: [^0].
|
|
dist < 102 ifTrue: [^0].
|
|
count := 0.
|
|
self rangeDo: [
|
|
:dpos :ddist |
|
|
(self validCheatFrom: pos dist: dist
|
|
to: dpos dist: ddist
|
|
withTime: time) ifTrue: [
|
|
count := count + 1 ]]
|
|
inManhDistance: time
|
|
from: pos
|
|
maxDist: dist - 102.
|
|
^count ]
|
|
|
|
"Answers whether POS -> DPOS is a cheat that indeed only takes
|
|
TIME ps, and saves more than 100 ps."
|
|
validCheatFrom: pos dist: dist
|
|
to: dpos dist: ddist
|
|
withTime: time
|
|
[ | ctime saved |
|
|
ctime := (dpos x - pos x) abs +
|
|
(dpos y - pos y) abs.
|
|
ctime > time ifTrue: [^false].
|
|
saved := dist - ddist - ctime.
|
|
^saved >= 100 ]
|
|
|
|
"Call BLOCK with DPOS and DDIST for each position within
|
|
DISTMH of POS, which is at most MSCORE away from the end.
|
|
BLOCK may be called with other positions."
|
|
OLDrangeDo: block
|
|
inManhDistance: distmh
|
|
from: pos
|
|
maxDist: mscore
|
|
[ | ddist dpos px py |
|
|
dpos := Posn new.
|
|
px := pos x. py := pos y.
|
|
distmh negated to: distmh do: [
|
|
:dx | distmh negated to: distmh do: [
|
|
:dy |
|
|
(dx abs + dy abs) <= distmh ifTrue: [
|
|
dpos x: px + dx; y: py + dy.
|
|
ddist := distToEnd at: dpos.
|
|
ddist ifNotNil: [
|
|
block value: dpos value: ddist ]]]] ]
|
|
|
|
"Optimisation of the above that uses the k-d tree to answer range queries."
|
|
rangeDo: block
|
|
inManhDistance: distmh
|
|
from: pos
|
|
maxDist: mscore
|
|
[ | min max dpos |
|
|
min := {0. pos x - distmh. pos y - distmh}.
|
|
max := {mscore. pos x + distmh. pos y + distmh}.
|
|
dpos := Posn new.
|
|
tree from: min to: max do: [
|
|
:ddist :dstx :dsty |
|
|
dpos x: dstx y: dsty.
|
|
block value: dpos value: ddist ] asSpreader ]
|
|
]
|
|
|
|
AOC input: [ Racetrack new rows: stdin toLines asArray ];
|
|
part1: 2; part2: 20;
|
|
result: [ :track :time | track countCheatsWithTime: time ];
|
|
finish.
|