diff --git a/2024/day6/Cargo.lock b/2024/day6/Cargo.lock new file mode 100644 index 0000000..a16c0bf --- /dev/null +++ b/2024/day6/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "day6" +version = "0.1.0" diff --git a/2024/day6/Cargo.toml b/2024/day6/Cargo.toml new file mode 100644 index 0000000..46c0229 --- /dev/null +++ b/2024/day6/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "day6" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/2024/day6/README.md b/2024/day6/README.md new file mode 100644 index 0000000..a7864b1 --- /dev/null +++ b/2024/day6/README.md @@ -0,0 +1,50 @@ +# Solution Day 6 + +This time 2D navigation. Love it... + +Setting up the infrastructure first: loading the map, turning it into something that can easily be navigated in 2D (i.e. _NOT +`Vec`, because it's basically impossible to access a single character in a string by index) and set up the start position. +What's missing then is a simulation function, that steps the guard by 1 field according to the rules of the game or returns +`None`, if the guard has left the field. + +This infrastructure takes up a significant amount of space in the code, but makes the actual implementation easier. + +## Task 1 + +I chose a rather straightforward solution: simulate the guard until it leaves the field and mark every step with an `X`. When +finished, count the number of X on the field and you're done. + +_Note:_ This does assume, that there are no loops for the guard in the input, which luckily is the case. Otherwise we would have +needed some kind of marking algorithm like in task 2 to prevent us from being stuck in an endless loop (and the task wouldn't make sense). + +## Task 2 + +This one was more tricky. The outer loops just try every possible position for a new obstacle and then run the algorithm to check, whether +the guard enters a loop. + +I initially thought about a simple marking algorithm for loop detection: mark every field where I take a turn and stop, when +I take a turn on that field again, assuming I was in a loop then. While this type of algorithm works reasonably well to check, whether +a graph is acyclic, it returns too many results for this particular problem (2194 to be precise in my input, instead of the correct 1602). +Reason being: I can _turn_ on a field where I took a turn before without entering a loop, _as long as it's not the exact same turn_. +So, the following is possible and not a loop: + +``` +..... +..#.. +..+#. +..|.. +..^.. +..|.. +``` + +Explanation: The guard goes up, hits the obstacle, turns to the right, immediately hits another obstacle, turns down and goes back the +was he came. Since my algorithm would mark the `+` field on the first turn, the immediate second turn would be classed as entering a +loop, even though the guard would end up facing a different direction afterwards. + +To fix this, I need to track, whether I've made the exact same turn before. So, instead of making my map structure more complex to +record all turns made somewhere, I just copy and save every turn step (position and new direction). If I make a turn and find, that +this step is already in the list of previous turns, I've actually entered a loop and can stop. This then leads to the correct number +of possible obstacle postions. + +One possible optimization would be to use the result map from task 1 to only check the positions, where the guard actually reaches, +but the runtime of the algorithms is still in the low seconds on my laptop, so, again, not worth the effort... \ No newline at end of file diff --git a/2024/day6/src/main.rs b/2024/day6/src/main.rs new file mode 100644 index 0000000..0c4037a --- /dev/null +++ b/2024/day6/src/main.rs @@ -0,0 +1,189 @@ +type Map = Vec>; + +fn load_map() -> Map { + let input = std::fs::read_to_string("input.txt").unwrap(); + let map: Map = input + .split("\n") + .map(|s| s.chars().collect::>()) + .filter(|line| line.len() > 0) + .collect(); + map +} + +#[derive(PartialEq, Eq, Clone, Debug)] +enum Direction { + Up, + Down, + Left, + Right, +} + +#[derive(Clone, Debug, PartialEq)] +struct GuardPosition { + x: usize, + y: usize, + direction: Direction, +} + +fn get_guard_position(map: &Map) -> GuardPosition { + for y in 0..map.len() { + for x in 0..map[y].len() { + if map[y][x] == '^' { + return GuardPosition { + x, + y, + direction: Direction::Up, + }; + } + } + } + panic!("Did not find guard start postion"); +} + +/// Update the guard position based on the guard movement rules. Returns the new position or None if the guard has left the map +fn step_guard(map: &Map, guard: GuardPosition) -> Option { + match guard.direction { + Direction::Down => { + if guard.y == map.len() - 1 { + None + } else if map[guard.y + 1][guard.x] == '#' { + // we could optimize the step by already moving where the guard will be next, but this saves us from having to check, whether we're at the edge already + Some(GuardPosition { + x: guard.x, + y: guard.y, + direction: Direction::Left, + }) + } else { + Some(GuardPosition { + x: guard.x, + y: guard.y + 1, + direction: Direction::Down, + }) + } + } + Direction::Up => { + if guard.y == 0 { + None + } else if map[guard.y - 1][guard.x] == '#' { + // we could optimize the step by already moving where the guard will be next, but this saves us from having to check, whether we're at the edge already + Some(GuardPosition { + x: guard.x, + y: guard.y, + direction: Direction::Right, + }) + } else { + Some(GuardPosition { + x: guard.x, + y: guard.y - 1, + direction: Direction::Up, + }) + } + } + Direction::Left => { + if guard.x == 0 { + None + } else if map[guard.y][guard.x - 1] == '#' { + // we could optimize the step by already moving where the guard will be next, but this saves us from having to check, whether we're at the edge already + Some(GuardPosition { + x: guard.x, + y: guard.y, + direction: Direction::Up, + }) + } else { + Some(GuardPosition { + x: guard.x - 1, + y: guard.y, + direction: Direction::Left, + }) + } + } + Direction::Right => { + if guard.x == map[guard.y].len() - 1 { + None + } else if map[guard.y][guard.x + 1] == '#' { + // we could optimize the step by already moving where the guard will be next, but this saves us from having to check, whether we're at the edge already + Some(GuardPosition { + x: guard.x, + y: guard.y, + direction: Direction::Down, + }) + } else { + Some(GuardPosition { + x: guard.x + 1, + y: guard.y, + direction: Direction::Right, + }) + } + } + } +} + +fn task1() { + let mut map = load_map(); + + let mut guard_opt = Some(get_guard_position(&map)); + loop { + if let Some(guard) = guard_opt { + map[guard.y][guard.x] = 'X'; + guard_opt = step_guard(&map, guard); + } else { + break; + } + } + + let places_touched = map.iter().fold(0, |sum, line| { + sum + line.iter().fold( + 0, + |line_sum, &ch| if ch == 'X' { line_sum + 1 } else { line_sum }, + ) + }); + + println!("Task 1: Places: {places_touched}"); +} + +fn task2() { + let map_orig = load_map(); + let mut possible_positions = 0; + + // try out all possible positions and add some obstacle there + for y in 0..map_orig.len() { + for x in 0..map_orig[y].len() { + let mut map = map_orig.clone(); + let mut guard_opt = Some(get_guard_position(&map)); + + if map[x][y] == '#' || map[x][y] == '^' { + continue; // no need to check here, there's already an obstacle or the guard + } + let current_element = map[x][y]; + map[x][y] = '#'; + let mut existing_turns = Vec::::new(); + loop { + if let Some(guard) = guard_opt.as_ref() { + let new_guard_pos = step_guard(&map, guard.clone()); + if let Some(np) = new_guard_pos.as_ref() { + if np.direction != guard.direction { + // we changed direction -> check whether we've changed the direction in the exact same way here before, which would mean we're in a loop + if existing_turns.contains(np) { + possible_positions += 1; + break; + } else { + existing_turns.push(np.clone()); // remember the turn + } + } + } + guard_opt = new_guard_pos; + } else { + break; + } + } + map[x][y] = current_element; + } + } + + println!("Task 2: Possible positions: {possible_positions}"); +} + +fn main() { + task1(); + task2(); +}