Post: Advent of code day 13
This commit is contained in:
parent
5c1b82e8b9
commit
268338e883
|
@ -0,0 +1,157 @@
|
||||||
|
---
|
||||||
|
title: "Packet Scanners — Haskell — #adventofcode Day 13"
|
||||||
|
description: "In which I get the job done."
|
||||||
|
slug: day-13
|
||||||
|
date: 2017-12-15T15:56:20+00:00
|
||||||
|
tags:
|
||||||
|
- Technology
|
||||||
|
- Learning
|
||||||
|
- Advent of Code
|
||||||
|
- Haskell
|
||||||
|
series: aoc2017
|
||||||
|
---
|
||||||
|
|
||||||
|
[Today's challenge](http://adventofcode.com/2017/day/13) requires us to sneak past a firewall made up of a series of scanners.
|
||||||
|
|
||||||
|
[→ Full code on GitHub](https://github.com/jezcope/aoc2017/blob/master/13-packet-scanners-redux.hs)
|
||||||
|
|
||||||
|
!!! commentary
|
||||||
|
I wasn't really thinking straight when I solved this challenge. I got a solution without too much trouble, but I ended up simulating the step-by-step movement of the scanners.
|
||||||
|
|
||||||
|
I finally realised that I could calculate whether or not a given scanner was safe at a given time directly with modular arithmetic, and it bugged me so much that I reimplemented the solution. Both are given below, the faster one first.
|
||||||
|
|
||||||
|
First we introduce some standard library stuff and define some useful utilities.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import Data.Maybe (mapMaybe)
|
||||||
|
|
||||||
|
strip :: String -> String
|
||||||
|
strip = T.unpack . T.strip . T.pack
|
||||||
|
|
||||||
|
splitOn :: String -> String -> [String]
|
||||||
|
splitOn sep = map T.unpack . T.splitOn (T.pack sep) . T.pack
|
||||||
|
|
||||||
|
parseScanner :: String -> (Int, Int)
|
||||||
|
parseScanner s = (d, r)
|
||||||
|
where [d, r] = map read $ splitOn ": " s
|
||||||
|
```
|
||||||
|
|
||||||
|
`traverseFW` does all the hard work: it checks for each scanner whether or not it's safe as we pass through, and returns a list of the severities of each time we're caught. `mapMaybe` is like the standard `map` in many languages, but operates on a list of Haskell `Maybe` values, like a combined `map` and `filter`. If the value is `Just x`, `x` gets included in the returned list; if the value is `Nothing`, then it gets thrown away.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
traverseFW :: Int -> [(Int, Int)] -> [Int]
|
||||||
|
traverseFW delay = mapMaybe caught
|
||||||
|
where
|
||||||
|
caught (d, r) = if (d + delay) `mod` (2*(r-1)) == 0
|
||||||
|
then Just (d * r)
|
||||||
|
else Nothing
|
||||||
|
```
|
||||||
|
|
||||||
|
Then the total severity of our passage through the firewall is simply the sum of each individual severity.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
severity :: [(Int, Int)] -> Int
|
||||||
|
severity = sum . traverseFW 0
|
||||||
|
```
|
||||||
|
|
||||||
|
But we don't want to know how badly we got caught, we want to know how long to wait before setting off to get through safely. `findDelay` tries traversing the firewall with increasing delay, and returns the delay for the first pass where we predict not getting caught.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
findDelay :: [(Int, Int)] -> Int
|
||||||
|
findDelay scanners = head $ filter (null . flip traverseFW scanners) [0..]
|
||||||
|
```
|
||||||
|
|
||||||
|
And finally, we put it all together and calculate and print the result.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
main = do
|
||||||
|
scanners <- fmap (map parseScanner . lines) getContents
|
||||||
|
|
||||||
|
putStrLn $ "Severity: " ++ (show $ severity scanners)
|
||||||
|
putStrLn $ "Delay: " ++ (show $ findDelay scanners)
|
||||||
|
```
|
||||||
|
|
||||||
|
I'm not generally bothered about performance for these challenges, but here I'll note that my second attempt runs in a little under 2 seconds on my laptop:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ time ./13-packet-scanners-redux < 13-input.txt
|
||||||
|
Severity: 1900
|
||||||
|
Delay: 3966414
|
||||||
|
./13-packet-scanners-redux < 13-input.txt 1.73s user 0.02s system 99% cpu 1.754 total
|
||||||
|
```
|
||||||
|
|
||||||
|
Compare that with the first, simulation-based one, which takes nearly a full minute:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ time ./13-packet-scanners < 13-input.txt
|
||||||
|
Severity: 1900
|
||||||
|
Delay: 3966414
|
||||||
|
./13-packet-scanners < 13-input.txt 57.63s user 0.27s system 100% cpu 57.902 total
|
||||||
|
```
|
||||||
|
|
||||||
|
And for good measure, here's the code. Notice the `tick` and `tickOne` functions, which together simulate moving all the scanners by one step; for this to work we have to track the full current state of each scanner, which is easier to read with a Haskell record-based custom data type. `traverseFW` is more complicated because it has to drive the simulation, but the rest of the code is mostly the same.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import Control.Monad (forM_)
|
||||||
|
|
||||||
|
data Scanner = Scanner { depth :: Int
|
||||||
|
, range :: Int
|
||||||
|
, pos :: Int
|
||||||
|
, dir :: Int }
|
||||||
|
instance Show Scanner where
|
||||||
|
show (Scanner d r p dir) = show d ++ "/" ++ show r ++ "/" ++ show p ++ "/" ++ show dir
|
||||||
|
|
||||||
|
strip :: String -> String
|
||||||
|
strip = T.unpack . T.strip . T.pack
|
||||||
|
|
||||||
|
splitOn :: String -> String -> [String]
|
||||||
|
splitOn sep str = map T.unpack $ T.splitOn (T.pack sep) $ T.pack str
|
||||||
|
|
||||||
|
parseScanner :: String -> Scanner
|
||||||
|
parseScanner s = Scanner d r 0 1
|
||||||
|
where [d, r] = map read $ splitOn ": " s
|
||||||
|
|
||||||
|
tickOne :: Scanner -> Scanner
|
||||||
|
tickOne (Scanner depth range pos dir)
|
||||||
|
| pos <= 0 = Scanner depth range (pos+1) 1
|
||||||
|
| pos >= range - 1 = Scanner depth range (pos-1) (-1)
|
||||||
|
| otherwise = Scanner depth range (pos+dir) dir
|
||||||
|
|
||||||
|
tick :: [Scanner] -> [Scanner]
|
||||||
|
tick = map tickOne
|
||||||
|
|
||||||
|
traverseFW :: [Scanner] -> [(Int, Int)]
|
||||||
|
traverseFW = traverseFW' 0
|
||||||
|
where
|
||||||
|
traverseFW' _ [] = []
|
||||||
|
traverseFW' layer scanners@((Scanner depth range pos _):rest)
|
||||||
|
-- | layer == depth && pos == 0 = (depth*range) + (traverseFW' (layer+1) $ tick rest)
|
||||||
|
| layer == depth && pos == 0 = (depth,range) : (traverseFW' (layer+1) $ tick rest)
|
||||||
|
| layer == depth && pos /= 0 = traverseFW' (layer+1) $ tick rest
|
||||||
|
| otherwise = traverseFW' (layer+1) $ tick scanners
|
||||||
|
|
||||||
|
severity :: [Scanner] -> Int
|
||||||
|
severity = sum . map (uncurry (*)) . traverseFW
|
||||||
|
|
||||||
|
empty :: [a] -> Bool
|
||||||
|
empty [] = True
|
||||||
|
empty _ = False
|
||||||
|
|
||||||
|
findDelay :: [Scanner] -> Int
|
||||||
|
findDelay scanners = delay
|
||||||
|
where
|
||||||
|
(delay, _) = head $ filter (empty . traverseFW . snd) $ zip [0..] $ iterate tick scanners
|
||||||
|
|
||||||
|
|
||||||
|
main = do
|
||||||
|
scanners <- fmap (map parseScanner . lines) getContents
|
||||||
|
|
||||||
|
putStrLn $ "Severity: " ++ (show $ severity scanners)
|
||||||
|
putStrLn $ "Delay: " ++ (show $ findDelay scanners)
|
||||||
|
```
|
Loading…
Reference in New Issue