Day 10: Pipe Maze
Megathread guidelines
- 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)
- Code block support is not fully rolled out yet but likely will be in the middle of the event. Try to share solutions as both code blocks and using something such as https://topaz.github.io/paste/ , pastebin, or github (code blocks to future proof it for when 0.19 comes out and since code blocks currently function in some apps and some instances as well if they are running a 0.19 beta)
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
🔒 Thread is locked until there's at least 100 2 star entries on the global leaderboard
🔓 Unlocked after 40 mins
Scala3
forgot to post this
import Area.* import Dir.* enum Dir(num: Int, diff: (Int, Int)): val n = num val d = diff case Up extends Dir(3, (0, -1)) case Down extends Dir(1, (0, 1)) case Left extends Dir(2, (-1, 0)) case Right extends Dir(0, (1, 0)) def opposite = Dir.from(n + 2) object Dir: def from(n: Int): Dir = Dir.all.filter(_.n == n % 4).ensuring(_.size == 1).head def all = List(Up, Down, Left, Right) enum Area: case Inside, Outside, Loop case class Pos(x: Int, y: Int) type Landscape = Map[Pos, Pipe] type Loop = Map[Pos, LoopPiece] def walk(p: Pos, d: Dir): Pos = Pos(p.x + d.d._1, p.y + d.d._2) val pipeMap = Map('|' -> List(Up, Down), '-' -> List(Left, Right), 'L' -> List(Up, Right), 'J' -> List(Up, Left), 'F' -> List(Right, Down), '7' -> List(Left, Down)) case class Pipe(neighbors: List[Dir]) case class LoopPiece(from: Dir, to: Dir): def left: List[Dir] = ((from.n + 1) until (if to.n < from.n then to.n + 4 else to.n)).map(Dir.from).toList def right: List[Dir] = LoopPiece(to, from).left def parse(a: List[String]): (Pos, Landscape) = val pipes = for (r, y) <- a.zipWithIndex; (v, x) <- r.zipWithIndex; p <- pipeMap.get(v) yield Pos(x, y) -> Pipe(p) val start = for (r, y) <- a.zipWithIndex; (v, x) <- r.zipWithIndex if v == 'S' yield Pos(x, y) (start.head, pipes.toMap) def walkLoop(start: Pos, l: Landscape): Loop = @tailrec def go(pos: Pos, last_dir: Dir, acc: Loop): Loop = if pos == start then acc else val dir = l(pos).neighbors.filter(_ != last_dir.opposite).ensuring(_.size == 1).head go(walk(pos, dir), dir, acc + (pos -> LoopPiece(last_dir.opposite, dir))) Dir.all.filter(d => l.get(walk(start, d)).exists(p => p.neighbors.contains(d.opposite))) match case List(start_dir, return_dir) => go(walk(start, start_dir), start_dir, Map(start -> LoopPiece(return_dir, start_dir))) case _ => Map() def task1(a: List[String]): Long = walkLoop.tupled(parse(a)).size.ensuring(_ % 2 == 0) / 2 def task2(a: List[String]): Long = val loop = walkLoop.tupled(parse(a)) val ys = a.indices val xs = a.head.indices val points = (for x <- xs; y <- ys yield Pos(x, y)).toSet // floodfill @tailrec def go(outside: Set[Pos], q: List[Pos]): Set[Pos] = if q.isEmpty then outside else val nbs = Dir.all.map(walk(q.head, _)).filter(points.contains(_)).filter(!outside.contains(_)) go(outside ++ nbs, nbs ++ q.tail) // start by floodfilling from the known outside: beyond the array bounds val boundary = ys.flatMap(y => List(Pos(-1, y), Pos(xs.end, y))) ++ xs.flatMap(x => List(Pos(x, -1), Pos(x, ys.end))) val r = go(boundary.toSet ++ loop.keySet, boundary.toList) // check on which side of the pipe the outside is, then continue floodfill from there val xsl = List[LoopPiece => List[Dir]](_.left, _.right).map(side => loop.flatMap((p, l) => side(l).map(d => walk(p, d))).filter(!loop.contains(_)).toSet).map(a => a -> a.intersect(r).size).ensuring(_.exists(_._2 == 0)).filter(_._2 != 0).head._1 (points -- go(r ++ xsl, xsl.toList)).size
Python
Also on Github.
from .solver import Solver _EXITS_MAP = { '|': ((0, -1), (0, 1)), '-': ((-1, 0), (1, 0)), 'L': ((1, 0), (0, -1)), 'J': ((-1, 0), (0, -1)), '7': ((-1, 0), (0, 1)), 'F': ((1, 0), (0, 1)), '.': (), 'S': (), } class Day10(Solver): def __init__(self): super().__init__(10) self.maze: dict[tuple[int, int], str] = {} self.start: tuple[int, int] = (0, 0) self.dists: dict[tuple[int, int], int] = {} def _pipe_has_exit(self, x: int, y: int, di: int, dj: int, inverse: bool = False) -> bool: if inverse: di, dj = -di, -dj return (di, dj) in _EXITS_MAP[self.maze[(x, y)]] def presolve(self, input: str): self.maze: dict[tuple[int, int], str] = {} self.start: tuple[int, int] = (0, 0) for y, line in enumerate(input.rstrip().split('\n')): for x, c in enumerate(line): self.maze[(x, y)] = c if c == 'S': self.start = (x, y) next_pos: list[tuple[int, int]] = [] directions_from_start = [] for di, dj in ((0, -1), (1, 0), (0, 1), (-1, 0)): x, y = self.start[0] + di, self.start[1] + dj if (x, y) not in self.maze: continue if not self._pipe_has_exit(x, y, di, dj, inverse=True): continue next_pos.append((x, y)) directions_from_start.append((di, dj)) self.maze[self.start] = [c for c, dmap in _EXITS_MAP.items() if set(directions_from_start) == set(dmap)][0] dists: dict[tuple[int, int], int] = {} cur_dist = 0 while True: cur_dist += 1 new_next_pos = [] for x, y in next_pos: if (x, y) in dists: continue dists[(x, y)] = cur_dist for di, dj in ((0, -1), (1, 0), (0, 1), (-1, 0)): nx, ny = x + di, y + dj if (nx, ny) not in self.maze: continue if not self._pipe_has_exit(x, y, di, dj): continue new_next_pos.append((nx, ny)) if not new_next_pos: break next_pos = new_next_pos self.dists = dists def solve_first_star(self) -> int: return max(self.dists.values()) def solve_second_star(self) -> int: area = 0 for y in range(max(y for _, y in self.dists.keys()) + 1): internal = False previous_wall = False wall_start_symbol = None for x in range(max(x for x, _ in self.dists.keys()) + 1): is_wall = (x, y) == self.start or (x, y) in self.dists wall_continues = is_wall pipe_type = self.maze[(x, y)] if is_wall and pipe_type == '|': internal = not internal wall_continues = False elif is_wall and not previous_wall and pipe_type in 'FL': wall_start_symbol = pipe_type elif is_wall and not previous_wall: raise RuntimeError(f'expecting wall F or L at {x}, {y}, got {pipe_type}') elif is_wall and previous_wall and pipe_type == 'J': wall_continues = False if wall_start_symbol == 'F': internal = not internal elif is_wall and previous_wall and pipe_type == '7': wall_continues = False if wall_start_symbol == 'L': internal = not internal elif not is_wall and previous_wall: raise RuntimeError(f'expecting wall J or 7 at {x}, {y}, got {pipe_type}') if internal and not is_wall: area += 1 previous_wall = wall_continues return area
Well, star one is solved. I don't love the code, but yet again, it works for now. I don't love the use of a label to continue/break a loop, and the
valid_steps
function is a mess that could probably be done much cleaner.Upon looking at star 2 I don't even have the slightest idea of where to start. I may have to come back to this one at a later date. Sigh.
https://github.com/capitalpb/advent_of_code_2023/blob/main/src/solvers/day10.rs
use crate::Solver; #[derive(Debug)] struct PipeMap { start: usize, tiles: Vec, width: usize, } impl PipeMap { fn from(input: &str) -> PipeMap { let tiles = input .lines() .rev() .flat_map(|row| row.chars()) .collect::>(); let width = input.find('\n').unwrap(); let start = tiles.iter().position(|tile| tile == &'S').unwrap(); PipeMap { start, tiles, width, } } fn valid_steps(&self, index: usize) -> Vec { let mut tiles = vec![]; let current_tile = *self.tiles.get(index).unwrap(); if "S|LJ".contains(current_tile) { let north = index + self.width; if let Some(tile) = self.tiles.get(north) { if "|7F".contains(*tile) { tiles.push(north); } } } if "S|7F".contains(current_tile) { if let Some(south) = index.checked_sub(self.width) { if let Some(tile) = self.tiles.get(south) { if "|LJ".contains(*tile) { tiles.push(south); } } } } if "S-J7".contains(current_tile) { if let Some(west) = index.checked_sub(1) { if (west % self.width) != (self.width - 1) { if let Some(tile) = self.tiles.get(west) { if "-LF".contains(*tile) { tiles.push(west); } } } } } if "S-LF".contains(current_tile) { let east = index + 1; if east % self.width != 0 { if let Some(tile) = self.tiles.get(east) { if "-J7".contains(*tile) { tiles.push(east); } } } } tiles } } pub struct Day10; impl Solver for Day10 { fn star_one(&self, input: &str) -> String { let pipe_map = PipeMap::from(input); let mut current_pos = pipe_map.start; let mut last_pos = pipe_map.start; let mut steps: usize = 0; 'outer: loop { for pos in pipe_map.valid_steps(current_pos) { if pos != last_pos { last_pos = current_pos; current_pos = pos; steps += 1; continue 'outer; } } break; } steps.div_ceil(2).to_string() } fn star_two(&self, input: &str) -> String { todo!() } }
I always felt I was one fix away from the solution, which was both nice and bad.
Walking the path was fine, and part 2 looked easy until I missed the squeezed pipes. I for some silly reason thought I only had to expand the grid by x2 instead of x3 and had to re-do that. Fill is hyper bad but works for <1 minute.
Python
import re import math import argparse import itertools from enum import Flag,Enum class Connection(Flag): Empty = 0b0000 North = 0b0001 South = 0b0010 East = 0b01000 West = 0b10000 def connected_directions(first:Connection,second:Connection) -> bool: return bool(((first.value >> 1) & second.value) or ((first.value << 1) & second.value)) def opposite_direction(dir:Connection) -> Connection: if dir.value & 0b00011: return Connection(dir.value ^ 0b00011) if dir.value & 0b11000: return Connection(dir.value ^ 0b11000) return Connection(0) class PipeElement: def __init__(self,symbol:chr) -> None: self.symbol = symbol self.connection = Connection.Empty if symbol in [*'|LJS']: self.connection |= Connection.North if symbol in [*'|7FS']: self.connection |= Connection.South if symbol in [*'-LFS']: self.connection |= Connection.East if symbol in [*'-J7S']: self.connection |= Connection.West if self.connection == Connection.Empty: self.symbol = '.' def __repr__(self) -> str: return f"Pipe({self.connection})" def __str__(self) -> str: return self.symbol def connected_to(self,pipe,direction:Connection) -> bool: if not (self.connection & direction): return False if self.connection & direction and pipe.connection & opposite_direction(direction): return True return False class Navigator: def __init__(self,list:list,width) -> None: self.list = list self.width = width def at(self,position): return self.list[position] def neighbor(self,position,direction:Connection) -> tuple | None: match direction: case Connection.North: return self.prev_row(position) case Connection.South: return self.next_row(position) case Connection.East: return self.next(position) case Connection.West: return self.prev(position) raise Exception(f"Direction not found: {direction}") def prev_row(self,position) -> tuple | None: p = position - self.width if p < 0: return None return (p,self.list[p]) def next_row(self,position) -> tuple | None: p = position + self.width if p >= len(self.list): return None return (p,self.list[p]) def prev(self,position) -> tuple | None: p = position - 1 if p < 0: return None return (p,self.list[p]) def next(self,position) -> tuple | None: p = position + 1 if p >= len(self.list): return None return (p,self.list[p]) def all_neighbors(self,position) -> list: l = list() for f in [self.next, self.prev, self.next_row,self.prev_row]: t = f(position) if t != None: l.append(t) return l def find_connected(self,position,exclude=Connection.Empty) -> tuple | None: for dir in [Connection.East,Connection.West,Connection.North,Connection.South]: if dir == exclude: continue n = self.neighbor(position,dir) if n == None: continue if self.at(position).connected_to(n[1],dir): return (*n,dir) return None class TileType(Enum): Inside = 1 Outside = 0 Pipe = 2 PlaceHolder = 3 def pipe_to_tile_expand(pipe:PipeElement) -> list: s = str(pipe) expansions = { '.': '.PP'+ 'PPP' + 'PPP', '-': 'PPP'+ '---' + 'PPP', '|': 'P|P'+ 'P|P' + 'P|P', 'F': 'PPP'+ 'PF-' + 'P|P', '7': 'PPP'+ '-7P' + 'P|P', 'J': 'P|P'+ '-JP' + 'PPP', 'L': 'P|P'+ 'PL-' + 'PPP', 'S': 'P|P'+ '-S-' + 'P|P' } l = expansions[s] return [pipe_to_tile(x) for x in [*l]] def pipe_to_tile(pipe:str) -> TileType: expansions = { '.': TileType.Inside, '-': TileType.Pipe, '|': TileType.Pipe, 'F': TileType.Pipe, '7': TileType.Pipe, 'J': TileType.Pipe, 'L': TileType.Pipe, 'S': TileType.Pipe, 'P': TileType.PlaceHolder } return expansions[pipe] def chunks(lst, n): """Yield successive n-sized chunks from lst.""" for i in range(0, len(lst), n): yield lst[i:i + n] def print_tiles(tile_list:list,width:int): for c in chunks(tile_list,width): print("".join([str(t.value) for t in c])) def print_pipes(tile_list:list,width:int): for c in chunks(tile_list,width): print("".join([str(t) for t in c])) def main(line_list:list,part:int): width = None pipe_list = list() tile_list = list() start_o = None for line in line_list: line = line + ' ' # stops east/west joining over new lines if width == None: width = len(line) for c in [*line]: o = PipeElement(c) pipe_list.append(o) tile_list.append(TileType.Inside) if c == 'S': start_o = o #print(pipe_list) start_pos = pipe_list.index(start_o) start_co = (start_pos // width, start_pos % width) print(f"starting index: {start_pos}: {start_co}") nav = Navigator(pipe_list,width) cur_pos = None last_dir = Connection.Empty steps = 0 while cur_pos != start_pos: if cur_pos == None: cur_pos = start_pos pipe = nav.find_connected(cur_pos,exclude=opposite_direction(last_dir)) if pipe == None: raise Exception(f"end of pipe at: {cur_pos}, {nav.at(cur_pos)}") cur_pos = pipe[0] last_dir = pipe[2] steps += 1 #print(f"{cur_pos}->",end="") tile_list[cur_pos] = TileType.Pipe print(f"end: {cur_pos}, steps: {steps}") clean_pipe = list() for i in range(0,len(pipe_list)): if tile_list[i] == TileType.Pipe: clean_pipe.append(pipe_list[i]) else: clean_pipe.append(PipeElement('.')) print_pipes(clean_pipe,width) print(f"part 1: {steps/2}") # part 2 outputs #print("start tile:") #print_tiles(tile_list,width) # add outsides to edge of map tile_list2 = list() #first row expanded_width = (width*3)+2 for i in range(0,expanded_width): tile_list2.append(TileType.Outside) for row in chunks(clean_pipe, width): ## we need to expand this to 2x size tiles t_rows = [ list() for x in range(0,3)] [ x.append(TileType.Outside) for x in t_rows] for r in row: parts = pipe_to_tile_expand(r) [ t_rows[x].extend( parts[x*3:(x*3)+3] ) for x in range(0,3)] [ x.append(TileType.Outside) for x in t_rows] [ tile_list2.extend(x) for x in t_rows] for i in range(0,expanded_width): tile_list2.append(TileType.Outside) #print("with o tile:") #print_tiles(tile_list2,width+2) tilenav = Navigator(tile_list2,expanded_width) changes = True while changes == True: changes = False count_in = 0 for i in range(0,len(tile_list2)): t = tilenav.at(i) if t == TileType.Inside or t == TileType.PlaceHolder: n = tilenav.all_neighbors(i) if any([x[1] == TileType.Outside for x in n]): tilenav.list[i] = TileType.Outside changes = True continue if t == TileType.Inside: count_in += 1 print("with outside tile:") print_tiles(tile_list2,expanded_width) print(count_in) if __name__ == "__main__": parser = argparse.ArgumentParser(description="template for aoc solver") parser.add_argument("-input",type=str) parser.add_argument("-part",type=int) args = parser.parse_args() filename = args.input if filename == None: parser.print_help() exit(1) part = args.part file = open(filename,'r') main([line.rstrip('\n') for line in file.readlines()],part) file.close()
Language: Python
Decided to use a graph to solve (which expanded the size). Part 1 was cycle detection, and part 2 was flooding of the outside.