|
| 1 | +This one is a cute little interpreter problem, a staple of advent of code. |
| 2 | +Let's write Part 1 in a way that makes Part 2 easy, where we will have to |
| 3 | +eventually "run" it backwards. We can use `Finite n` as the type with `n` |
| 4 | +inhabitants, so `Finite 8` will, for example, have the numbers 0 to 7. And also |
| 5 | +`Vector n a` from `Data.Vector.Sized`, which contains `n` items. |
| 6 | + |
| 7 | +```haskell |
| 8 | +data Combo |
| 9 | + = CLiteral (Finite 4) |
| 10 | + | CReg (Finite 3) |
| 11 | + |
| 12 | +data Instr |
| 13 | + = ADV Combo |
| 14 | + | BXL (Finite 8) |
| 15 | + | BST Combo |
| 16 | + | JNZ (Finite 4) |
| 17 | + | BXC |
| 18 | + | OUT Combo |
| 19 | + | BDV Combo |
| 20 | + | CDV Combo |
| 21 | +``` |
| 22 | + |
| 23 | +We can then write a function to interpret the outputs into a monoid. |
| 24 | + |
| 25 | +```haskell |
| 26 | +stepWith :: |
| 27 | + Monoid a => |
| 28 | + Vector 8 Instr -> |
| 29 | + -- | out |
| 30 | + (Finite 8 -> a) -> |
| 31 | + -- | Starting a |
| 32 | + Word -> |
| 33 | + -- | Starting b |
| 34 | + Word -> |
| 35 | + -- | Starting c |
| 36 | + Word -> |
| 37 | + a |
| 38 | +stepWith prog out = go 0 |
| 39 | + where |
| 40 | + go i !a !b !c = case prog `SV.index` i of |
| 41 | + ADV r -> withStep go (a `div` (2 ^ combo r)) b c |
| 42 | + BXL l -> withStep go a (b `xor` fromIntegral l) c |
| 43 | + BST r -> withStep go a (combo r `mod` 8) c |
| 44 | + JNZ l |
| 45 | + | a == 0 -> withStep go 0 b c |
| 46 | + | otherwise -> go (weakenN l) a b c -- weakenN :: Finite 4 -> Finite 8 |
| 47 | + BXC -> withStep go a (b `xor` c) c |
| 48 | + OUT r -> |
| 49 | + let o = modulo (fromIntegral (combo r)) |
| 50 | + in out o <> withStep go a b c |
| 51 | + BDV r -> withStep go a (a `div` (2 ^ combo r)) c |
| 52 | + CDV r -> withStep go a b (a `div` (2 ^ combo r)) |
| 53 | + where |
| 54 | + combo = \case |
| 55 | + CLiteral l -> fromIntegral l |
| 56 | + CReg 0 -> a |
| 57 | + CReg 1 -> b |
| 58 | + CReg _ -> c |
| 59 | + withStep p |
| 60 | + | i == maxBound = \_ _ _ -> mempty |
| 61 | + | otherwise = p (i + 1) |
| 62 | +``` |
| 63 | + |
| 64 | +Part 1 is a straightforward application, although we can use a difflist to get |
| 65 | +O(n) concats instead of O(n^2) |
| 66 | + |
| 67 | +```haskell |
| 68 | +import Data.DList as DL |
| 69 | + |
| 70 | +part1 :: Vector 8 Instr -> Word -> Word -> Word -> [Finite 8] |
| 71 | +part1 prog a b c = DL.toList $ stepWith prog DL.singleton a b c |
| 72 | +``` |
| 73 | + |
| 74 | +Part 2 it gets a bit interesting. We can solve it "in general" under the |
| 75 | +conditions: |
| 76 | + |
| 77 | + |
| 78 | +1. The final instruction is JNZ 0 |
| 79 | +2. There is one `OUT` per loop, with a register |
| 80 | +3. b and c are overwritten at the start of each loop |
| 81 | + |
| 82 | +The plan would be: |
| 83 | + |
| 84 | + |
| 85 | +1. Start from the end with a known `a` and move backwards, accumulating all |
| 86 | + possible values of `a` that would lead to the end value, ignoring b and c |
| 87 | +2. For each of those possible a's, start from the beginning with that `a` and |
| 88 | + filter the ones that don't produce the correct `OUT`. |
| 89 | + |
| 90 | +We have to write a "step backwards" from scratch, but we can actually use our |
| 91 | +original `stepWith` to write a version that _bails_ after the first output, by |
| 92 | +having our monoid be `Data.Monoid.First`. Then in the line `out o <> withStep |
| 93 | +go a abc`, it'll just completely ignore the right hand side and output the |
| 94 | +first `OUT` result. |
| 95 | + |
| 96 | +```haskell |
| 97 | +searchStep :: Vector 8 Instr -> [Finite 8] -> [Word] |
| 98 | +searchStep prog outs = do |
| 99 | + -- enforce the invariants |
| 100 | + JNZ 0 <- pure $ prog `SV.index` maxBound |
| 101 | + [CReg _] <- pure [r | OUT r <- toList prog] |
| 102 | + search 0 (reverse outs) |
| 103 | + where |
| 104 | + search a = \case |
| 105 | + o : os -> do |
| 106 | + a' <- stepBack a |
| 107 | + guard $ stepForward a' == Just o |
| 108 | + search a' os |
| 109 | + [] -> pure a |
| 110 | + -- doesn't enforce that b and c are reset, because i'm lazy |
| 111 | + stepForward :: Word -> Maybe (Finite 8) |
| 112 | + stepForward a0 = getFirst $ stepWith tp (First . Just) a0 0 0 |
| 113 | + stepBack :: Word -> [Word] |
| 114 | + stepBack = go' maxBound |
| 115 | + where |
| 116 | + go' i a = case tp `SV.index` i of |
| 117 | + ADV r -> do |
| 118 | + a' <- case r of |
| 119 | + CLiteral l -> ((a `shift` fromIntegral l) +) <$> [0 .. 2 ^ getFinite l - 1] |
| 120 | + CReg _ -> [] |
| 121 | + go' (pred i) a' |
| 122 | + OUT _ -> pure a |
| 123 | + _ -> go' (pred i) a |
| 124 | +``` |
| 125 | + |
| 126 | +We really only have to handle the `ADV r` case because that's the only |
| 127 | +instruction that modifies `A`. If we `ADV 3`, that means that the possible |
| 128 | +"starting A's" are `known_a * 8 + x`, where `x` is between 0 and 7. |
| 129 | + |
| 130 | +Wrapping it all up: |
| 131 | + |
| 132 | +```haskell |
| 133 | +part2 :: Vector 8 Instr -> [Finite 8] -> Maybe Word |
| 134 | +part2 instrs = listToMaybe . searchStep instrs |
| 135 | +``` |
0 commit comments