Quest 12: One Spark to Burn Them All
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
Link to participate: https://everybody.codes/
You must log in or # to comment.
Rust
use std::collections::{HashMap, HashSet}; use itertools::Itertools; fn explode_from_barrel( map: &HashMap<(isize, isize), i8>, mut exploded: HashSet<(isize, isize)>, mut front: HashSet<(isize, isize)>, ) -> HashSet<(isize, isize)> { while !front.is_empty() { let mut next_front = HashSet::new(); for (i, j) in front.drain() { exploded.insert((i, j)); let my_size = map[&(i, j)]; for (di, dj) in [(-1, 0), (0, -1), (0, 1), (1, 0)] { let (ni, nj) = (i + di, j + dj); if exploded.contains(&(ni, nj)) { continue; } if let Some(neighour_size) = map.get(&(ni, nj)) && *neighour_size <= my_size { next_front.insert((ni, nj)); } } } front = next_front; } exploded } pub fn solve_part_1(input: &str) -> String { let map: HashMap<(isize, isize), i8> = input .lines() .enumerate() .flat_map(|(i, line)| { line.chars() .enumerate() .map(move |(j, ch)| ((i as isize, j as isize), ch as i8 - '0' as i8)) }) .collect(); let exploded = HashSet::<(isize, isize)>::new(); let front: HashSet<(isize, isize)> = [(0, 0)].into_iter().collect(); explode_from_barrel(&map, exploded, front).len().to_string() } pub fn solve_part_2(input: &str) -> String { let map: HashMap<(isize, isize), i8> = input .lines() .enumerate() .flat_map(|(i, line)| { line.chars() .enumerate() .map(move |(j, ch)| ((i as isize, j as isize), ch as i8 - '0' as i8)) }) .collect(); let exploded = HashSet::<(isize, isize)>::new(); let max_i = map.keys().map(|(i, _)| *i).max().unwrap(); let max_j = map.keys().map(|(_, j)| *j).max().unwrap(); let front: HashSet<(isize, isize)> = [(0, 0), (max_i, max_j)].into_iter().collect(); explode_from_barrel(&map, exploded, front).len().to_string() } pub fn solve_part_3(input: &str) -> String { let map: HashMap<(isize, isize), i8> = input .lines() .enumerate() .flat_map(|(i, line)| { line.chars() .enumerate() .map(move |(j, ch)| ((i as isize, j as isize), ch as i8 - '0' as i8)) }) .collect(); let max_i = map.keys().map(|(i, _)| *i).max().unwrap(); let max_j = map.keys().map(|(_, j)| *j).max().unwrap(); let best_barrel = (0..=max_i) .cartesian_product(0..=max_j) .map(|(i, j)| { ((i, j), { let exploded = HashSet::<(isize, isize)>::new(); let front: HashSet<(isize, isize)> = [(i, j)].into_iter().collect(); explode_from_barrel(&map, exploded, front) }) }) .max_by_key(|(_, exploded)| exploded.len()) .unwrap(); let second_best_barrel = (0..=max_i) .cartesian_product(0..=max_j) .filter(|&(i, j)| !best_barrel.1.contains(&(i, j))) .map(|(i, j)| { ((i, j), { let exploded = best_barrel.1.clone(); let front: HashSet<(isize, isize)> = [(i, j)].into_iter().collect(); explode_from_barrel(&map, exploded, front) }) }) .max_by_key(|(_, exploded)| exploded.len()) .unwrap(); let third_best_barrel = (0..=max_i) .cartesian_product(0..=max_j) .filter(|&(i, j)| !second_best_barrel.1.contains(&(i, j))) .map(|(i, j)| { ((i, j), { let exploded = second_best_barrel.1.clone(); let front: HashSet<(isize, isize)> = [(i, j)].into_iter().collect(); explode_from_barrel(&map, exploded, front) }) }) .max_by_key(|(_, exploded)| exploded.len()) .unwrap(); let exploded = HashSet::<(isize, isize)>::new(); let front: HashSet<(isize, isize)> = [best_barrel.0, second_best_barrel.0, third_best_barrel.0] .into_iter() .collect(); explode_from_barrel(&map, exploded, front).len().to_string() }Python
# get all valid neighbors of a cell in a grid DELTAS = [(-1, 0), (1, 0), (0, -1), (0, 1)] def getNeighbors(i, j, m, n): for di, dj in DELTAS: ni, nj = i + di, j + dj if 0 <= ni < m and 0 <= nj < n: yield ni, nj # constant for marking permanently burnt cells PERMA_BURNT = -1 # DFS to burn the grid from a list of starting points # If perma_burn is True, cells that are burnt will be modified to PERMA_BURNT # returns the set of all burnt cells def dfs_burn(grid: list[list[int]], start: list[tuple[int, int]], perma_burn=False) -> set[tuple[int, int]]: m, n = len(grid), len(grid[0]) ignited = start burnt = set() while ignited: i, j = ignited.pop() if (i, j) in burnt: continue burnt.add((i, j)) for ni, nj in getNeighbors(i, j, m, n): if (ni, nj) in burnt or grid[ni][nj] == PERMA_BURNT: continue if grid[ni][nj] <= grid[i][j]: ignited.append((ni, nj)) # mark as permanently burnt (if requested) if perma_burn: grid[i][j] = PERMA_BURNT return burnt # Part 1: DFS burn from single source (0, 0) def part1(data: str) -> int: grid = [[int(c) for c in row] for row in data.splitlines()] return len(dfs_burn(grid, [(0, 0)])) assert part1("""989601 857782 746543 766789""") == 16 # Part 2: DFS burn from two sources (0,0) and (m-1,n-1) def part2(data: str) -> int: grid = [[int(c) for c in row] for row in data.splitlines()] m, n = len(grid), len(grid[0]) return len(dfs_burn(grid, [(0, 0), (m - 1, n - 1)])) assert part2("""9589233445 9679121695 8469121876 8352919876 7342914327 7234193437 6789193538 6781219648 5691219769 5443329859""") == 58 from copy import deepcopy # Part 3: Greedy selection of three best burn points def part3(data: str) -> int: grid = [[int(c) for c in row] for row in data.splitlines()] m, n = len(grid), len(grid[0]) # keep original grid for final burn count og_grid = deepcopy(grid) # generate the list of best burn candidates # note that the cell with the max value is not necessarily the single best burn point # this will help us skip the cells that are clearly suboptimal candidates = [(grid[i][j], i, j) for i in range(m) for j in range(n)] candidates.sort(reverse=True) # best three burn points (separately chosen) best_three = [] for _ in range(3): # set of all cells burnt in this iteration burnt = set() # track the best burn location in this iteration max_burnt = 0 max_burn_loc = None for _, i, j in candidates: # skip candidates that are already burnt by a previous burn point # this is because a previous burn point will contain all the burns of this candidate # plus possibly more if (i, j) in burnt: continue # burn (impermanently) from this candidate burns = dfs_burn(grid, [(i, j)]) if len(burns) > max_burnt: max_burnt = len(burns) max_burn_loc = (i, j) # record newly burnt cells to skip in future candidates burnt |= burns assert max_burn_loc is not None, "Should have found a max burn location" # record and permanently burn from the best location found dfs_burn(grid, [max_burn_loc], perma_burn=True) best_three.append(max_burn_loc) # return total burnt cells from all three best burn points simultaneously return len(dfs_burn(og_grid, best_three)) assert (t := part3("""5411 3362 5235 3112""")) == 14, f"Expected: 14, Actual: {t}" assert (t := part3("""41951111131882511179 32112222211518122215 31223333322115122219 31234444432147511128 91223333322176121892 61112222211166431583 14661111166111111746 11111119142122222177 41222118881233333219 71222127839122222196 56111126279711111517""")) == 136, f"Expected: 136, Actual: {t}"The “is between” operator in Python is perhaps the thing I miss the most from Python :)
That and the
else(no-break) clause for loops are some of my favorite features.

