@@ -1950,32 +1950,142 @@ Day 20
1950
1950
1951
1951
[ d20c ] : https://github.com/mstksg/advent-of-code-2017/blob/master/src/AOC2017/Day20.hs
1952
1952
1953
+ Day 20 starts out as a simple physics simulator/numerical integrator:
1954
+
1953
1955
``` haskell
1954
- data S = S { _sPos :: ! (V. Vector (Maybe (L. V3 Int )))
1955
- , _sVel :: ! (V. Vector (Maybe (L. V3 Int )))
1956
- , _sAcc :: ! (V. Vector (Maybe (L. V3 Int )))
1957
- }
1958
- deriving Show
1956
+ type Point = L. V3 Int
1957
+
1958
+ data Particle a = P { _pAcc :: ! a
1959
+ , _pVel :: ! a
1960
+ , _pPos :: ! a
1961
+ }
1962
+ deriving (Functor , Foldable , Traversable , Show , Eq , Ord )
1963
+
1964
+ type System = [Particle Point ]
1965
+ ```
1966
+
1967
+ Using the * [linear][] * package again, for `V3 `, a 3 - vector. It's also
1968
+ convenient to decide a `Particle ` to contain a description of its acceleration,
1969
+ velocity, and position. Our whole system will be a list of `Particle Point `s.
1970
+ Note that we parameterize `Particle ` so that we can give useful higher- kinded
1971
+ instances like `Functor ` and `Traversable `.
1972
+
1973
+ Stepping the simulation ends up being just stepping every particle.
1974
+ Interestingly enough, we can actually use `scanl (+) 0 ` (for `Traversable `) to
1975
+ do the integration step:
1976
+
1977
+ ```haskell
1978
+ -- | scanl generalized to work on all Traversable
1979
+ scanlT :: Traversable t => (b -> a -> b ) -> b -> t a -> t b
1980
+ scanlT = -- implementatation left as exercise, but I really wish this was
1981
+ -- already in base :|
1982
+
1983
+ step :: Num a => Particle a -> Particle a
1984
+ step = scanlT (+) 0
1985
+ ```
1986
+
1987
+ This is because it replaces ` _pAcc ` with ` 0 + _pAcc ` , and then it replaces
1988
+ ` _pVel ` with ` 0 + _pAcc + _pVel ` , and then finally replaces ` _pPos ` with `0 +
1989
+ _ pAcc + _ pVel + _ pPos` -- just like the problem asks!
1990
+
1991
+ For part 1, we can just ` map step ` a ` System ` several points, and then find
1992
+ closest point:
1993
+
1994
+ ``` haskell
1995
+ norm :: Point -> Int
1996
+ norm = sum . fmap abs
1997
+
1998
+ day20a :: System -> Int
1999
+ day20a = V. minIndex . V. fromList -- hijacking minIndex from Vector
2000
+ . map (norm . _pPos)
2001
+ . (!! 1000 )
2002
+ . iterate (map step)
2003
+ ```
2004
+
2005
+ However, we could also be sneaky and just find the "maximum" normed initial
2006
+ vector, which is correct in the case where all of our initial accelerations are
2007
+ differently normed, and sometimes correct in the case where we have duplicated
2008
+ accelerations.
2009
+
2010
+ ``` haskell
2011
+ day20a :: System -> Int
2012
+ day20a = V. minIndex . V. fromList
2013
+ . (map . fmap ) norm
2014
+ . parse
2015
+ ```
2016
+
2017
+ For part 2, we can define a function that takes out all "duplicated" points,
2018
+ using a frequency map and filtering for frequencies greater than 1:
2019
+
2020
+ ``` haskell
2021
+ collide :: System -> System
2022
+ collide s0 = filter ((`S.notMember` collisions) . _pPos) s0
2023
+ where
2024
+ collisions :: S. Set Point
2025
+ collisions = M. keysSet . M. filter @ Int (> 1 )
2026
+ . M. fromListWith (+)
2027
+ . map ((,1 ) . _pPos)
2028
+ $ toList s0
2029
+ ```
2030
+
2031
+ Now we just iterate ` collide . map step ` .
2032
+
2033
+ We can pick the thousandth element again, like we might have for part 1.
2034
+ However, we can be a little smart with a stopping condition:
2035
+
2036
+ ``` haskell
2037
+ day20b :: Challenge
2038
+ day20b = show . length . fromJust . find stop
2039
+ . iterate (collide . map step)
2040
+ . parse
2041
+ where
2042
+ stop = (> 1000 ) . minimum . map (norm . _sPos)
2043
+ ```
2044
+
2045
+ Here, we iterate until the particle * closest* to the origin is greater than
2046
+ a 1000-cube away from the origin. Essentially, this is waiting until all of
2047
+ the points clear a 2000-wide cube around the origin. Thinking about the input,
2048
+ there will be some particles that start out near the origin and start heading
2049
+ * towards* the origin. This condition will wait until the last of those
2050
+ particles exits the origin cube, and check for the number of collisions then.
2051
+
2052
+ ### Parsing
2053
+
2054
+ We can parse into ` System ` using really silly view patterns :)
2055
+
2056
+ ``` haskell
2057
+ parse :: String -> System
2058
+ parse = map parseLine . lines
2059
+ where
2060
+ parseLine :: String -> Particle Point
2061
+ parseLine (map (read . filter numChar). splitOn" ," -> [pX,pY,pZ,vX,vY,vZ,aX,aY,aZ])
2062
+ = P { _pAcc = L. V3 aX aY aZ
2063
+ , _pVel = L. V3 vX vY vZ
2064
+ , _pPos = L. V3 pX pY pZ
2065
+ }
2066
+ parseLine _ = error " No parse"
2067
+ numChar :: Char -> Bool
2068
+ numChar c = isDigit c || c == ' -'
1959
2069
```
1960
2070
1961
2071
### Day 20 Benchmarks
1962
2072
1963
2073
```
1964
2074
>> Day 20a
1965
2075
benchmarking...
1966
- time 33.64 ms (30.59 ms .. 37.78 ms)
1967
- 0.957 R2 (0.920 R2 .. 0.998 R2)
1968
- mean 33.70 ms (32.37 ms .. 36.27 ms)
1969
- std dev 3.692 ms (1.222 ms .. 5.172 ms)
1970
- variance introduced by outliers: 42 % (moderately inflated)
2076
+ time 29.87 ms (28.16 ms .. 32.18 ms)
2077
+ 0.989 R2 (0.979 R2 .. 0.997 R2)
2078
+ mean 33.94 ms (32.33 ms .. 38.26 ms)
2079
+ std dev 5.085 ms (1.678 ms .. 8.683 ms)
2080
+ variance introduced by outliers: 61 % (severely inflated)
1971
2081
1972
2082
>> Day 20b
1973
2083
benchmarking...
1974
- time 74.06 ms (68.03 ms .. 84.42 ms)
1975
- 0.964 R2 (0.879 R2 .. 0.997 R2)
1976
- mean 76.05 ms (72.32 ms .. 83.21 ms)
1977
- std dev 7.736 ms (4.126 ms .. 11.56 ms)
1978
- variance introduced by outliers: 29 % (moderately inflated)
2084
+ time 67.18 ms (64.33 ms .. 72.99 ms)
2085
+ 0.990 R2 (0.975 R2 .. 0.998 R2)
2086
+ mean 66.90 ms (64.83 ms .. 68.67 ms)
2087
+ std dev 3.437 ms (2.439 ms .. 4.727 ms)
2088
+ variance introduced by outliers: 16 % (moderately inflated)
1979
2089
```
1980
2090
1981
2091
Day 21
0 commit comments