2023-12-08 06:30:11 +01:00
#![ feature(test) ]
extern crate test ;
use aoc2023 ::{ boilerplate , common ::* } ;
2023-12-08 10:06:19 +01:00
use fnv ::FnvHashMap as HashMap ;
2023-12-08 06:30:11 +01:00
2023-12-08 10:06:19 +01:00
const DAY : usize = 8 ;
type Parsed < ' a > = ( Vec < Direction > , HashMap < & ' a str , [ & ' a str ; 2 ] > ) ;
2023-12-08 06:30:11 +01:00
2023-12-08 07:05:56 +01:00
#[ derive(Debug, PartialEq, Eq, Copy, Clone, Hash) ]
2023-12-08 06:30:11 +01:00
enum Direction {
2023-12-08 10:06:19 +01:00
Left = 0 ,
Right = 1 ,
2023-12-08 06:30:11 +01:00
}
fn parse_input ( raw : & str ) -> Parsed {
let ( directions , map ) = raw . split_once ( " \n \n " ) . unwrap ( ) ;
let directions = directions
. bytes ( )
. map ( | i | match i {
b 'L' = > Direction ::Left ,
b 'R' = > Direction ::Right ,
_ = > unreachable! ( ) ,
} )
. collect ( ) ;
2023-12-08 10:06:19 +01:00
let map = map . lines ( ) . map ( | l | ( & l [ 0 ..= 2 ] , [ & l [ 7 ..= 9 ] , & l [ 12 ..= 14 ] ] ) ) . collect ( ) ;
2023-12-08 06:30:11 +01:00
( directions , map )
}
2023-12-08 07:05:56 +01:00
fn steps_until ( ( directions , map ) : & Parsed , start : & str , target : & str ) -> usize {
2023-12-08 06:30:11 +01:00
directions
. iter ( )
. cycle ( )
2023-12-08 07:05:56 +01:00
. scan ( start , | pos , dir | {
2023-12-08 10:06:19 +01:00
let next = map . get ( pos ) ? [ * dir as usize ] ;
2023-12-08 08:54:41 +01:00
( ! next . ends_with ( target ) ) . then ( | | * pos = next )
2023-12-08 06:30:11 +01:00
} )
. count ( )
+ 1
}
2023-12-08 08:54:41 +01:00
fn part1 ( parsed : & Parsed ) -> usize {
steps_until ( parsed , " AAA " , " ZZZ " )
}
2023-12-08 07:05:56 +01:00
// I’m honestly not sure why this works. It seems each path only has a single ghost node, and that
// node occurs right before looping, so we can just compute the least common multiple of their step counts.
// I assume this holds true for other inputs (it can’t have been random),
// but I don’t see it anywhere in the task and only found out by experimentation.
2023-12-08 06:30:11 +01:00
fn part2 ( parsed : & Parsed ) -> usize {
2023-12-08 09:49:45 +01:00
parsed . 1. keys ( ) . filter ( | start | start . ends_with ( " A " ) ) . fold ( 1 , | acc , n | lcm ( acc , steps_until ( parsed , n , " Z " ) ) )
2023-12-08 06:30:11 +01:00
}
2023-12-08 07:05:56 +01:00
#[ cfg(test) ]
const TEST_INPUT_P2 : & str = " LR
11 A = ( 11 B , XXX )
11 B = ( XXX , 11 Z )
11 Z = ( 11 B , XXX )
22 A = ( 22 B , XXX )
22 B = ( 22 C , 22 C )
22 C = ( 22 Z , 22 Z )
22 Z = ( 22 B , 22 B )
XXX = ( XXX , XXX ) " ;
2023-12-08 06:30:11 +01:00
boilerplate! {
TEST_INPUT = = " LLR
AAA = ( BBB , BBB )
BBB = ( AAA , ZZZ )
ZZZ = ( ZZZ , ZZZ ) " ,
tests : {
part1 : { TEST_INPUT = > 6 } ,
2023-12-08 07:05:56 +01:00
part2 : { TEST_INPUT_P2 = > 6 } ,
2023-12-08 06:30:11 +01:00
} ,
bench1 = = 12083 ,
2023-12-08 07:05:56 +01:00
bench2 = = 13385272668829 ,
2023-12-08 10:06:19 +01:00
bench_parse : | ( v , m ) : & Parsed | { assert_eq! ( m [ " AAA " ] , [ " MJJ " , " QBJ " ] ) ; ( v . len ( ) , v [ 0 ] ) } = > ( 281 , Direction ::Left ) ,
2023-12-08 06:30:11 +01:00
}