-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathDay15Beacons.hs
103 lines (89 loc) · 3.5 KB
/
Day15Beacons.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
import qualified Data.Ix as I
import Data.List (foldl', partition, sort)
import Data.Maybe (mapMaybe)
import qualified Data.Set as S
import Data.Void (Void)
import Formatting (fprintLn, int, (%))
import Text.Megaparsec
import Text.Megaparsec.Char (eol, space)
import Text.Megaparsec.Char.Lexer (decimal, signed)
type Parser = Parsec Void String
type Point = (Int, Int)
data Sensor = Sensor
{ sensor :: Point,
beacon :: Point
}
deriving (Show)
parseSensor :: Parser Sensor
parseSensor = do
chunk "Sensor at "
sensor <- parsePoint
chunk ": closest beacon is at "
beacon <- parsePoint
eol
return $ Sensor {sensor, beacon}
parsePoint :: Parser Point
parsePoint = (,) <$> (chunk "x=" *> signedInt) <*> (chunk ", y=" *> signedInt)
where
signedInt = signed space decimal
manhattanDistance :: Point -> Point -> Int
manhattanDistance (x1, y1) (x2, y2) = abs (x2 - x1) + abs (y2 - y1)
offLimits :: Int -> Sensor -> Maybe (Int, Int)
offLimits yCoord Sensor {sensor = sensor@(x, y), beacon}
| distToRow > dist = Nothing
| otherwise = Just (x - maxHorizontal, x + maxHorizontal)
where
dist = manhattanDistance sensor beacon
distToRow = abs (y - yCoord)
-- width of illegal range decreases by 1 on each side for every unit away from the sensor
maxHorizontal = dist - distToRow
fuseRanges :: [(Int, Int)] -> (Int, Int) -> [(Int, Int)]
fuseRanges rs range
| null redundant = range : fine
| otherwise = fused : fine
where
-- fusable ranges either overlap or are adjacent to each other
fusable a@(a1, a2) b@(b1, b2) = I.inRange b a1 || I.inRange a b1 || a1 == b2 + 1 || b1 == a2 + 1
(redundant, fine) = partition (fusable range) rs
(starts, ends) = unzip (range : redundant)
fused = (minimum starts, maximum ends)
offLimitsRanges :: [Sensor] -> Int -> [(Int, Int)]
offLimitsRanges sensors rowY = foldl' fuseRanges [] ranges
where
ranges = mapMaybe (offLimits rowY) sensors
offLimitsInRow :: [Sensor] -> Int -> Int
offLimitsInRow sensors rowY = overestimate - beaconOverlap
where
ranges = offLimitsRanges sensors rowY
overestimate = sum $ map I.rangeSize ranges
beaconOverlap = beaconsInRanges ranges rowY (map beacon sensors)
beaconsInRanges :: [(Int, Int)] -> Int -> [Point] -> Int
beaconsInRanges ranges rowY beacons = S.size overlapSet
where
inRowRange (x, y) = y == rowY && any (flip I.inRange x) ranges
overlapSet = S.fromList $ filter inRowRange beacons
findBeacon :: [Sensor] -> Point
findBeacon sensors = go [0 .. 4000000]
where
truncateRanges = map (\(start, end) -> (max start 0, min end 4000000))
go [] = error "No beacon found"
go (y : ys)
-- if there's two ranges, that means they couldn't be fused & are therefore disjoint
-- the x coordinate of the beacon will be 1 more than the end of the lower of the ranges
| length truncated == 2 = (,y) . (1 +) . snd . head $ sort truncated
| otherwise = go ys
where
truncated = truncateRanges $ offLimitsRanges sensors y
main :: IO ()
main = do
let inputFile = "input/day15.txt"
input <- readFile inputFile
let Right sensors = runParser (some parseSensor) inputFile input
illegalCount = offLimitsInRow sensors 2000000
(bx, by) = findBeacon sensors
tuningFreq = bx * 4000000 + by
fprintLn ("Part 1 (no-zones at y=2000000): " % int) illegalCount -- Expected: 4811413
fprintLn ("Part 2 (tuning frequency): " % int) tuningFreq -- Expected: 13171855019123