use std::collections::HashSet; use std::collections::HashMap; use std::fs; use std::env; use crate::Dir::{North, South, East, West }; // nodes are (coord, direction) pairs. // neighbors are step1+turnleft, step2+turnleft, step3+turnleft, step1+turnright, etc #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] enum Dir { North, South, East, West } fn step(start: &Coord, d: Dir, steps: i32) -> Coord { match d { North => (start.0, start.1 - steps), South => (start.0, start.1 + steps), East => (start.0 + steps, start.1), West => (start.0 - steps, start.1), } } type Coord = (i32, i32); fn main() { println!("Hello, AoC day 04!"); let args: Vec = env::args().collect(); if args.len() != 2 { println!("wrong number of arguments!"); std::process::exit(1); } let file_path = &args[1]; let contents = fs::read_to_string(file_path).expect("Should have been able to read the file"); let mut grid: HashMap = HashMap::new(); let mut x; // build our grid! let mut y = 0; for line in contents.lines() { x = 0; for c in line.chars() { let coords = (x,y); grid.insert(coords, c); x += 1; } y += 1; } // part a let mut sum = 0; let mut cache: Vec>= Vec::new(); for (location, _color) in &grid { cache = fill_region_and_cache(location, &grid, cache); } println!("we found {} regions", cache.len()); for region in &cache { println!("a region with color {} was found to have {} squares", grid.get(region.iter().nth(0).unwrap()).unwrap(), region.len()); } for region in &cache { let mut total_perimeter = 0; let mut area = 0; for location in region { total_perimeter += perimeter(&location, &grid); area += 1; } sum += area * total_perimeter; } println!("the total cost is {sum}"); // part b // a list of sides (a side is a set of sidesegments) let mut sides_cache: Vec> = Vec::new(); // for every location in the grid, for (location, _color) in &grid { // for every side ajdacent to that location, update the side cache with its side. for sideseg in sides(location, &grid) { sides_cache = spread_over_side_and_cache( &sideseg, &grid, sides_cache); } } sum = 0; for region in &cache { let mut sides = 0; let mut area = 0; for side in &sides_cache { let first_location = side.iter().nth(0).unwrap().1; if region.contains(&first_location) { sides += 1; } } area += region.len() as i32; let color = grid.get(region.iter().nth(0).unwrap()).unwrap(); println!("a region with label {color} has area {area} and {sides} sides"); sum += area * sides; } println!("the total cost is {sum}"); } fn neighbors(head: &Coord) -> Vec { return vec![North, South, East, West] .into_iter() .map(|x| step(head, x, 1)) .collect(); } fn same_neighbors(location: &Coord, grid: &HashMap) -> Vec { if let Some(me) = grid.get(location) { return vec![North, South, East, West] .into_iter() .map(|x| step(location, x, 1)) .filter(|x| grid.contains_key(x)) .filter(|x| me == grid.get(x).expect("tried to look for a missing grid square")) .collect(); } return vec![]; } fn fill_region_and_cache( location: &Coord, grid: &HashMap, mut cache: Vec> ) -> Vec> { let mut new_region: HashSet = HashSet::from([*location]); let mut accumulator: HashSet = HashSet::new(); while !new_region.is_empty() { let mut blah: HashSet = HashSet::new(); for x in &new_region { for existing_region in &cache { if existing_region.contains(x) { return cache; } } } for x in &new_region { for n in same_neighbors(&x, grid) { if !accumulator.contains(&n) { blah.insert(n); } } } for x in new_region.drain() { accumulator.insert(x); } new_region = blah; } cache.push(accumulator); return cache; } fn spread_over_side_and_cache( side: &SideSegment, grid: &HashMap, mut cache: Vec> ) -> Vec> { let mut wavefront: HashSet = HashSet::from([*side]); let mut accumulator: HashSet = HashSet::new(); while !wavefront.is_empty() { // we're iterating over the wavefront of squares to look at for whether they're // part of the side we're building. let mut new_wavefront: HashSet = HashSet::new(); // look at each side in the wavefront for x in &wavefront { // if it's in a side that we already have cached, then we're done. // this loop is what makes this slow i bet for existing_side in &cache { if existing_side.contains(x) { // return the cache unchanged. return cache; } } } // look at each side in the wavefrnot, to build up a new wavefront. for SideSegment(dir, coord) in &wavefront { for n in same_neighbors(&coord, grid) // look at neighbors in this region. .into_iter() .flat_map(|next_coord| sides(&next_coord, grid)) // look at sides of those neighbors. .filter(|SideSegment(dir2, _coord2)| dir2 == dir) { // filter down to just sides in the same direction. // n is a neighboring side with the same direction as us. if !accumulator.contains(&n) { // if n isn't already in the accumulator, add n to the new wavefront. new_wavefront.insert(n); } } } // add all the siddes in the wavefront to the accumulator. for x in &wavefront { accumulator.insert(*x); } wavefront = new_wavefront; } cache.push(accumulator); return cache; } fn perimeter(location: &Coord, grid: &HashMap) -> i32 { let mut perimeter = 0; for neighbor in neighbors(location) { let neighboring_region = grid.get(&neighbor); if neighboring_region != grid.get(location) { perimeter += 1; } } return perimeter; } #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] struct SideSegment (Dir, Coord); fn sides(location: &Coord, grid: &HashMap) -> Vec { return vec![North, South, East, West] .into_iter() .map(|x| SideSegment(x, *location) ) .filter(|SideSegment(dir, loc)| grid.get(loc) != grid.get(&step(loc, *dir, 1)) ) .collect(); }