Day 24: Crossed Wires
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
Haskell, programmatic solution
I spent an entire day on this because I didn’t write a unit test to check my “swap outputs” function, which effectively did nothing.
In any case: the approach (which may be more interesting than the code, I know people were interested) involved probing the addition circuit with some example additions - that is, I wrote something that’d let me give alternative inputs from x & y and compute the result using the circuit. I then gave it some simple pairs of values that’d exercise the add and carry bits (ie, pairs chosen from {i << n, n <- {1..43}, i <- {1, 3}}
). That gave me some breaking trials.
Because the errors were relatively sparse, I then scanned over pairs of outputs, swapping those that didn’t introduce a data dependency and checking (a) that no new errors were introduced over the trial sets, (b) for any reduction in the number of errors found. I got a bunch fo outputs like this:
swap of ("ccp","mnh") improves matters
bad trial count reduced from 346 to 344
which found the pairs for me. The search could be improved by more carefully tying the probe inputs to the outputs’ dependencies (ie, if the first error comes from the (xi, yi) input bits, then look for swaps of the dependencies introduced by zi) - but in any case, it finds the answer. Phew.
Haskell bits and pieces
The nice thing about Haskell’s laziness (assuming you use Data.Map rather than Data.Map.Strict) is that the laziness can do a ton of the work for you - you might’ve spotted a few Haskell solutions in earlier days’ threads that use this kind of trick (eg for tabling/memoisation). Here’s my evaluation function:
eval l =
let
v = l & Map.map (\case
Const x -> x
And a b -> v Map.! a && v Map.! b
Or a b -> v Map.! a || v Map.! b
Xor a b -> v Map.! a /= v Map.! b)
in v
For part 2, we know what the graph should look like (it’s just a binary adder); I think this is a maximal common subgraph problem, but I’m still reading around that at the mo. I’d love to know if there’s a trick to this.
Thank you for showing this trick, I knew Haskell was lazy but this one blew my mind again.
Haskell
For completeness’ sake. I actually solved part 2 by looking at the structure with Graphviz and checking the input manually for errors. So the code here merely replicates the checks I was doing by hand.
solution
import Control.Arrow
import Control.Monad
import Data.Bifoldable
import Data.Bits
import Data.List
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Maybe
import Data.Set (Set)
import Data.Set qualified as Set
import Text.Printf
data Op = AND | OR | XOR deriving (Read, Show, Eq)
readInput :: String -> (Map String Int, Map String (Op, (String, String)))
readInput s =
let (inputs, gates) = second (drop 1) $ break null $ lines s
in ( Map.fromList $ map (break (== ':') >>> (id *** read . drop 2)) inputs,
Map.fromList $ map (words >>> \[a, op, b, _, o] -> (o, (read op, (a, b)))) gates
)
evalNetwork :: Map String Int -> Map String (Op, (String, String)) -> Maybe Int
evalNetwork inputs gates = fromBits <$> getOutput signals
where
getOutput = traverse snd . takeWhile (("z" `isPrefixOf`) . fst) . Map.toDescList
fromBits = foldl' (\a b -> (a `shiftL` 1) .|. b) 0
signals = Map.union (Just <$> inputs) $ Map.mapWithKey getSignal gates
getSignal w (op, (a, b)) = doGate op <$> join (signals Map.!? a) <*> join (signals Map.!? b)
doGate AND = (.&.)
doGate OR = (.|.)
doGate XOR = xor
findError :: [(String, (Op, (String, String)))] -> Maybe (String, String)
findError gates = findGate AND ("x00", "y00") >>= go 1 . fst
where
go i carryIn = do
let [x, y, z] = map (: printf "%02d" (i :: Int)) ['x', 'y', 'z']
xor1 <- fst <$> findGate XOR (x, y)
and1 <- fst <$> findGate AND (x, y)
let layer2 = findGates (carryIn, xor1) ++ findGates (carryIn, and1)
xorGate2 <- find ((== XOR) . fst . snd) layer2
andGate2 <- find ((== AND) . fst . snd) layer2
let xor2 = fst xorGate2
and2 = fst andGate2
orGate <-
find
( \(_, (op, (a, b))) ->
op == OR && any (`elem` [a, b]) [xor1, and1, xor2, and2]
)
gates
msum
[ checkIs xor1 =<< otherInput carryIn xorGate2,
checkIs z xor2,
go (succ i) (fst orGate)
]
checkIs p q = (p, q) <$ guard (p /= q)
otherInput x (_, (_, (a, b)))
| a == x = Just b
| b == x = Just a
| otherwise = Nothing
findGates (a, b) = filter (\(_, (_, ins)) -> ins `elem` [(a, b), (b, a)]) gates
findGate op = find ((== op) . fst . snd) . findGates
part2 = sort . concatMap biList . unfoldr go . Map.assocs
where
go gates = (\p -> (p, first (exchange p) <$> gates)) <$> findError gates
exchange (a, b) c
| c == a = b
| c == b = a
| otherwise = c
main = do
(inputs, gates) <- readInput <$> readFile "input24"
print . fromJust $ evalNetwork inputs gates
putStrLn . intercalate "," $ part2 gates
Rust + Pen and Paper
Yikers. Part 2 took a while, staring at this diagram for hours. Eventually I noticed that each of these blocks has two pairs of (XOR, AND) gates sharing the same inputs (and inputs aren’t changed). So I matched up these pairs based on a distance metric of how much needs to be swapped to fit together. This helped me identify 4 blocks with errors, the rest was solved using pen and paper (one block is missing as it became apparent at that point):
There is also some code, but do yourself and me a favor and don’t look at it. While it does turn up the correct solution, it probably won’t with any other input, especially not the examples.
Haskell
Part 1 was trivial, just apply the operations and delay certain ones until you have all the inputs you need.
Code
import Control.Arrow
import Data.Bits
import Numeric
import qualified Data.Char as Char
import qualified Data.List as List
import qualified Data.Map as Map
parse s = (Map.fromList inputs, equations)
where
ls = lines s
inputs = map (take 3 &&& (== "1") . drop 5) . takeWhile (/= "") $ ls
equations = map words . filter (/= "") . tail . dropWhile (/= "") $ ls
operations = Map.fromList
[ ("AND", (&&))
, ("XOR", xor)
, ("OR", (||))
]
solveEquations is [] = is
solveEquations is (e:es)
| is Map.!? input1 == Nothing = solveEquations is (es ++ [e])
| is Map.!? input2 == Nothing = solveEquations is (es ++ [e])
| otherwise = solveEquations (Map.insert output (opfunc value1 value2) is) es
where
value1 = is Map.! input1
value2 = is Map.! input2
opfunc = operations Map.! operation
(input1:operation:input2:_:output:[]) = e
wireNumber prefix = List.filter ((prefix `List.isPrefixOf`) . fst)
>>> flip zip [0..]
>>> List.filter (snd . fst)
>>> List.map ((2 ^ ). snd)
>>> sum
part1 = uncurry solveEquations
>>> Map.toList
>>> wireNumber "z"
part2 (is, es) = List.intercalate "," . List.sort . words $ "z08 ffj dwp kfm z22 gjh jdr z31"
main = getContents
>>= print
. (part1 &&& part2)
. parse
For part 2 I tried symbolic solving to detect discrepancies but I wouldn’t achieve anything with it.
SymbolicEquation
data SymbolicEquation = Single { eqName :: String }
| Combine
{ eqName :: String
, eqOperation :: String
, eqLeft :: SymbolicEquation
, eqRight :: SymbolicEquation
}
deriving (Eq)
instance Show SymbolicEquation where
show (Single name) = name
show (Combine name op l r) = "(" ++ name ++ "= " ++ show l ++ " " ++ op ++ " " ++ show r ++ ")"
symbolicSolve is [] = is
symbolicSolve is (e:es)
| is Map.!? input1 == Nothing = symbolicSolve is (es ++ [e])
| is Map.!? input2 == Nothing = symbolicSolve is (es ++ [e])
| otherwise = symbolicSolve (Map.insert output (Combine output operation value1 value2) is) es
where
value1 = is Map.! input1
value2 = is Map.! input2
(input1:operation:input2:_:output:[]) = e
My solution was to use the dotEngine
-function to translate the operations into a digraph in graphviz-style which I simply plotted and searched through using a python script.
dotEngine
dotEngine (input1:operation:input2:_:output:[]) = [
input1 ++ " -> " ++ output ++ " [ label=" ++ operation ++ "];"
, input2 ++ " -> " ++ output ++ " [ label=" ++ operation ++ "];"
]
I took a loook at the initial graph which was a vertical line with a few exception which I figured would be the misordered wires.
I did try some hardware-simulations in the far past to build bit-adders which helped me recognize patterns like carry calculation.
First I replaced all occurences of x__ XOR y__ -> w
with x__ XOR y__ -> xor__
to recognize them more easily. The same with AND
of xs and ys.
Using the following script I would then use some Regex to search for the rules that corresponded to carry calculations or structures I knew. The script would break exactly four times and I would then figure out what to switch by hand through looking at the updated graphViz.
Please excuse the bad coding style in the script, I had written it on the ipython-REPL.
python script
r = open("input").read()
for i in range(2, 45):
prevI = str(i - 1).zfill(2)
I = str(i).zfill(2)
forward = f"xor{I} AND carry{prevI} -> (\\w+)"
backward = f"carry{prevI} AND xor{I} -> (\\w+)"
m1 = re.search(forward, r)
m2 = re.search(backward, r)
if m1 is None and m2 is None:
print(forward, backward)
break
m = m1 or m2
r = r.replace(m.group(1), f"combinedCarry{I}")
forward = f"and{I} OR combinedCarry{I} -> (\\w+)"
backward = f"combinedCarry{I} OR and{I} -> (\\w+)"
m1 = re.search(forward, r)
m2 = re.search(backward, r)
if m1 is None and m2 is None:
print(forward, backward)
break
m = m1 or m2
r = r.replace(m.group(1), f"carry{I}")
open("input", "w").write()
When solving such a swapped wire problem I would then use my haskell function to plot it out again and stare at it for a few minutes until I understood wich parts belonged where.
The last one looked like this
In this one I needed to switch jdr
and carry31
to make it work.