This commit is contained in:
Markus Brueckner 2024-12-06 19:38:14 +01:00
parent 7abbc37a65
commit 51dc523cf6
4 changed files with 252 additions and 0 deletions

7
2024/day6/Cargo.lock generated Normal file
View file

@ -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"

6
2024/day6/Cargo.toml Normal file
View file

@ -0,0 +1,6 @@
[package]
name = "day6"
version = "0.1.0"
edition = "2021"
[dependencies]

50
2024/day6/README.md Normal file
View file

@ -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<String>`, 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...

189
2024/day6/src/main.rs Normal file
View file

@ -0,0 +1,189 @@
type Map = Vec<Vec<char>>;
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::<Vec<char>>())
.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<GuardPosition> {
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::<GuardPosition>::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();
}