Day 6
This commit is contained in:
parent
7abbc37a65
commit
51dc523cf6
4 changed files with 252 additions and 0 deletions
7
2024/day6/Cargo.lock
generated
Normal file
7
2024/day6/Cargo.lock
generated
Normal 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
6
2024/day6/Cargo.toml
Normal file
|
@ -0,0 +1,6 @@
|
|||
[package]
|
||||
name = "day6"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
50
2024/day6/README.md
Normal file
50
2024/day6/README.md
Normal 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
189
2024/day6/src/main.rs
Normal 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();
|
||||
}
|
Loading…
Add table
Reference in a new issue