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

  • cvttsd2si@programming.dev
    ·
    7 months ago

    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
    
  • hades@lemm.ee
    ·
    7 months ago

    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
    
  • capitalpb@programming.dev
    ·
    7 months ago

    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!()
        }
    }
    
  • purplemonkeymad@programming.dev
    ·
    7 months ago

    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) &amp; second.value) or
                ((first.value &lt;&lt; 1) &amp; second.value))
    
    def opposite_direction(dir:Connection) -> Connection:
        if dir.value &amp; 0b00011:
            return Connection(dir.value ^ 0b00011)
        if dir.value &amp; 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 &amp; direction):
                return False
            
            if self.connection &amp; direction and pipe.connection &amp; 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 &lt; 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 &lt; 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()
    
  • Massahud@programming.dev
    ·
    7 months ago

    Language: Python

    Github

    Decided to use a graph to solve (which expanded the size). Part 1 was cycle detection, and part 2 was flooding of the outside.