use crate::input::Input;
#[cfg(feature = "visualization")]
use crate::painter::PainterRef;
use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
const DIRECTIONS: &[(i32, i32); 4] = &[(0, 1), (0, -1), (-1, 0), (1, 0)];
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)]
struct Key {
value: u8,
}
type KeyBitset = u32;
impl Key {
const fn new(value: u8) -> Self {
Self { value }
}
const fn bit_mask(self) -> KeyBitset {
1 << (self.value as usize - 'a' as usize)
}
}
struct Edge {
target_key: Key,
steps: usize,
needed_keys: KeyBitset,
#[cfg(feature = "visualization")]
x: i32,
#[cfg(feature = "visualization")]
y: i32,
}
pub fn steps_to_gather_all_keys(
input_string: &str,
#[cfg(feature = "visualization")] mut painter: &mut PainterRef,
#[cfg(feature = "visualization")] map_x_offset: usize,
#[cfg(feature = "visualization")] map_y_offset: usize,
#[cfg(feature = "visualization")] mut global_cols: usize,
#[cfg(feature = "visualization")] mut global_rows: usize,
) -> Result<usize, String> {
#[cfg(feature = "visualization")]
painter.fill_style_rgb(255, 0, 0);
let rows = input_string.lines().count();
let cols = input_string.lines().next().ok_or("Empty input")?.len();
let mut map = vec![b'#'; rows * cols];
let mut found_keys = HashMap::new();
let mut all_keys_bitset = 0 as KeyBitset;
let index_of = |x, y| x + y * cols;
#[cfg(feature = "visualization")]
{
if global_rows == 0 {
global_rows = rows;
global_cols = cols;
}
}
for (y, line) in input_string.lines().enumerate() {
if line.len() != cols {
return Err("Not all rows have same width".to_string());
}
line.chars().enumerate().for_each(|(x, c)| {
let byte = c as u8;
let current_position = (x as i32, y as i32);
#[cfg(feature = "visualization")]
let canvas_x = (x + map_x_offset) as f64 / global_cols as f64;
#[cfg(feature = "visualization")]
let canvas_y = (y + map_y_offset) as f64 / global_rows as f64;
#[cfg(feature = "visualization")]
let draw_width = 0.95 / global_cols as f64;
#[cfg(feature = "visualization")]
let draw_height = 0.95 / global_rows as f64;
#[cfg(feature = "visualization")]
let draw = |drawer: &mut PainterRef| {
drawer.fill_rect(canvas_x, canvas_y, draw_width, draw_height);
};
let char_to_insert = match c {
'@' => {
found_keys.insert(Key::new(b'@'), current_position);
#[cfg(feature = "visualization")]
{
painter.fill_style_rgb(0, 0, 255);
draw(&mut painter);
}
b'.'
}
'a'..='z' => {
let found_key = Key::new(byte);
all_keys_bitset |= found_key.bit_mask();
found_keys.insert(found_key, current_position);
#[cfg(feature = "visualization")]
{
painter.fill_style_rgb(0, 255, 0);
draw(&mut painter);
}
byte
}
'#' => {
#[cfg(feature = "visualization")]
{
painter.fill_style_rgb(255, 0, 0);
draw(&mut painter);
}
return;
}
_ => {
#[cfg(feature = "visualization")]
{
if ('A'..='Z').contains(&c) {
painter.fill_style_rgb(0, 255, 255);
draw(&mut painter);
}
}
byte
}
};
map[index_of(x, y)] = char_to_insert;
});
}
#[cfg(feature = "visualization")]
painter.end_frame();
if !found_keys.contains_key(&Key::new(b'@')) {
return Err("No entrance ('@') found".to_string());
}
let mut adjacency_list: HashMap<Key, Vec<Edge>> = HashMap::new();
for (&this_key, &this_key_position) in found_keys.iter() {
let mut to_visit = VecDeque::new();
to_visit.push_back((this_key_position, 0_u32, 0_u32));
let mut visited_positions = HashSet::new();
visited_positions.insert(this_key_position);
while let Some((position, needed_keys, steps)) = to_visit.pop_front() {
'key_direction_loop: for direction in DIRECTIONS.iter() {
let new_position = (position.0 + direction.0, position.1 + direction.1);
if new_position.0 < 0 || new_position.1 < 0 {
continue 'key_direction_loop;
}
let mut new_needed_keys = needed_keys;
let mut found_key = None;
match map.get(index_of(new_position.0 as usize, new_position.1 as usize)) {
Some(&char_at_position @ b'A'..=b'Z') => {
let needed_key = Key::new(char_at_position.to_ascii_lowercase());
if found_keys.contains_key(&needed_key) {
new_needed_keys |= needed_key.bit_mask();
}
}
Some(&char_at_position @ b'a'..=b'z') => {
found_key = Some(Key::new(char_at_position));
}
Some(b'.') => {
}
_ => {
continue 'key_direction_loop;
}
}
let new_steps = steps + 1;
if let Some(target_key) = found_key {
adjacency_list
.entry(this_key)
.or_insert_with(Vec::new)
.push(Edge {
steps: new_steps as usize,
needed_keys: new_needed_keys,
target_key,
#[cfg(feature = "visualization")]
x: new_position.0,
#[cfg(feature = "visualization")]
y: new_position.1,
});
} else if visited_positions.insert(new_position) {
let new_state = (new_position, new_needed_keys, new_steps);
to_visit.push_back(new_state);
}
}
}
}
shortest_path(
&adjacency_list,
all_keys_bitset,
#[cfg(feature = "visualization")]
&mut painter,
#[cfg(feature = "visualization")]
map_x_offset,
#[cfg(feature = "visualization")]
map_y_offset,
#[cfg(feature = "visualization")]
global_cols,
#[cfg(feature = "visualization")]
global_rows,
)
.ok_or_else(|| "Not possible to gather all keys".to_string())
}
fn shortest_path(
adjacency_list: &HashMap<Key, Vec<Edge>>,
all_keys: KeyBitset,
#[cfg(feature = "visualization")] drawer: &mut PainterRef,
#[cfg(feature = "visualization")] map_x_offset: usize,
#[cfg(feature = "visualization")] map_y_offset: usize,
#[cfg(feature = "visualization")] global_cols: usize,
#[cfg(feature = "visualization")] global_rows: usize,
) -> Option<usize> {
#[derive(Copy, Clone, Eq, PartialEq)]
struct Vertex {
at_key: Key,
steps: usize,
gathered_keys: KeyBitset,
#[cfg(feature = "visualization")]
x: i32,
#[cfg(feature = "visualization")]
y: i32,
}
impl Ord for Vertex {
fn cmp(&self, other: &Self) -> Ordering {
other
.steps
.cmp(&self.steps)
.then_with(|| self.gathered_keys.cmp(&other.gathered_keys))
.then_with(|| self.at_key.cmp(&other.at_key))
}
}
impl PartialOrd for Vertex {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
let mut cost_for_keys: HashMap<(Key, KeyBitset), usize> = HashMap::new();
let mut to_visit = BinaryHeap::new();
#[cfg(feature = "visualization")]
let mut visited_locations = HashSet::new();
to_visit.push(Vertex {
at_key: Key::new(b'@'),
steps: 0,
gathered_keys: 0,
#[cfg(feature = "visualization")]
x: 0,
#[cfg(feature = "visualization")]
y: 0,
});
while let Some(current) = to_visit.pop() {
if current.gathered_keys == all_keys {
return Some(current.steps);
}
#[cfg(feature = "visualization")]
{
if visited_locations.insert((current.x, current.y)) && current.at_key.value != b'@' {
let canvas_x = (current.x + map_x_offset as i32) as f64 / global_cols as f64;
let canvas_y = (current.y + map_y_offset as i32) as f64 / global_rows as f64;
let draw_width = 0.95 / global_cols as f64;
let draw_height = 0.95 / global_rows as f64;
drawer.fill_style_rgb(80, 0, 80);
drawer.fill_rect(canvas_x, canvas_y, draw_width, draw_height);
drawer.meta_delay(50);
}
}
for edge in adjacency_list.get(¤t.at_key)? {
let all_needed_keys_gathered =
edge.needed_keys & current.gathered_keys == edge.needed_keys;
if !all_needed_keys_gathered {
continue;
}
let next = Vertex {
steps: current.steps + edge.steps,
at_key: edge.target_key,
gathered_keys: current.gathered_keys | edge.target_key.bit_mask(),
#[cfg(feature = "visualization")]
x: edge.x,
#[cfg(feature = "visualization")]
y: edge.y,
};
let current_cost = cost_for_keys
.entry((edge.target_key, next.gathered_keys))
.or_insert(usize::max_value());
if next.steps < *current_cost {
to_visit.push(next);
*current_cost = next.steps;
}
}
}
None
}
pub fn solve(input: &mut Input) -> Result<usize, String> {
if input.is_part_one() {
return steps_to_gather_all_keys(
input.text,
#[cfg(feature = "visualization")]
&mut input.painter,
#[cfg(feature = "visualization")]
0,
#[cfg(feature = "visualization")]
0,
#[cfg(feature = "visualization")]
0,
#[cfg(feature = "visualization")]
0,
);
}
let mut map_top_left = String::new();
let mut map_top_right = String::new();
let mut map_bottom_left = String::new();
let mut map_bottom_right = String::new();
let num_rows = input.text.lines().count();
let num_columns = input
.text
.lines()
.next()
.ok_or("Invalid input - empty first line")?
.len();
let center_y = num_rows / 2;
let center_x = num_columns / 2;
input.text.lines().enumerate().for_each(|(y, line)| {
line.chars().enumerate().for_each(|(x, c)| {
let replaced_char = match (center_x as i32 - x as i32, center_y as i32 - y as i32) {
(0, 0) | (1, 0) | (-1, 0) | (0, 1) | (0, -1) => '#',
(1, 1) | (1, -1) | (-1, 1) | (-1, -1) => '@',
_ => c,
};
if y <= center_y {
if x <= center_x {
&mut map_top_left
} else {
&mut map_top_right
}
} else if x <= center_x {
&mut map_bottom_left
} else {
&mut map_bottom_right
}
.push(replaced_char);
});
if y <= center_y {
map_top_left.push('\n');
map_top_right.push('\n');
} else {
map_bottom_left.push('\n');
map_bottom_right.push('\n');
}
});
if !(map_top_left.starts_with('#')) {
return Err("Invalid input (not surrounded by '#')".to_string());
}
let s1 = steps_to_gather_all_keys(
&map_top_left,
#[cfg(feature = "visualization")]
&mut input.painter,
#[cfg(feature = "visualization")]
0,
#[cfg(feature = "visualization")]
0,
#[cfg(feature = "visualization")]
num_columns,
#[cfg(feature = "visualization")]
num_rows,
)?;
let s2 = steps_to_gather_all_keys(
&map_top_right,
#[cfg(feature = "visualization")]
&mut input.painter,
#[cfg(feature = "visualization")]
{
center_x + 1
},
#[cfg(feature = "visualization")]
0,
#[cfg(feature = "visualization")]
num_columns,
#[cfg(feature = "visualization")]
num_rows,
)?;
let s3 = steps_to_gather_all_keys(
&map_bottom_left,
#[cfg(feature = "visualization")]
&mut input.painter,
#[cfg(feature = "visualization")]
0,
#[cfg(feature = "visualization")]
{
center_y + 1
},
#[cfg(feature = "visualization")]
num_columns,
#[cfg(feature = "visualization")]
num_rows,
)?;
let s4 = steps_to_gather_all_keys(
&map_bottom_right,
#[cfg(feature = "visualization")]
&mut input.painter,
#[cfg(feature = "visualization")]
{
center_x + 1
},
#[cfg(feature = "visualization")]
{
center_y + 1
},
#[cfg(feature = "visualization")]
num_columns,
#[cfg(feature = "visualization")]
num_rows,
)?;
Ok(s1 + s2 + s3 + s4)
}
#[test]
pub fn tests_part1() {
assert_eq!(
solve(&mut Input::part_one(
"#########
#b.A.@.a#
#########"
)),
Ok(8)
);
assert_eq!(
solve(&mut Input::part_one(
"########################
#f.D.E.e.C.b.A.@.a.B.c.#
######################.#
#d.....................#
########################"
)),
Ok(86)
);
assert_eq!(
solve(&mut Input::part_one(include_str!("day18_input.txt"))),
Ok(4248)
);
}
#[test]
fn tests_part2() {
assert_eq!(
solve(&mut Input::part_two(include_str!("day18_input.txt"))),
Ok(1878)
);
}