Day 6: Guard Gallivant

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)
  • 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

FAQ

  • Pyro@programming.dev
    link
    fedilink
    arrow-up
    4
    ·
    edit-2
    6 days ago

    Python

    Part 1: Simulate the guard’s walk, keeping track of visited positions
    Part 2: Semi brute-force. Try to place an obstacle at every valid position in the guard’s original path and see if it leads to a loop.

    import os
    from collections import defaultdict
    
    # paths
    here = os.path.dirname(os.path.abspath(__file__))
    filepath = os.path.join(here, 'input.txt')
    
    # read input
    with open(filepath, mode='r', encoding='utf8') as f:
        data = f.read()
    rows = data.splitlines()
    
    # bounds
    m = len(rows)
    n = len(rows[0])
    
    # directions following 90 degree clockwise turns
    #   up, right, down, left
    DIRECTIONS = [(-1, 0), (0, 1), (1, 0), (0, -1)]
    
    # find position of guard
    guard_i, guard_j = -1, -1
    for i in range(m):
        for j in range(n):
            if rows[i][j] == '^':
                guard_i, guard_j = i, j
                break
        if guard_i != -1:
            break
    
    
    def part1(guard_i, guard_j):
        # keep track of visited positions
        visited = set()
        visited.add((guard_i, guard_j))
    
        dir_idx = 0     # current direction index
    
        # loop while guard is in map
        while True:
            delta_i, delta_j = DIRECTIONS[dir_idx]
            next_gi, next_gj = guard_i + delta_i, guard_j + delta_j   # next pos
            
            # if out of bounds, we are done
            if not (0 <= next_gi < m) or not (0 <= next_gj < n):
                break
            # change direction when obstacle encountered
            if rows[next_gi][next_gj] == "#":
                dir_idx = (dir_idx + 1) % 4
                continue
            # update position and visited
            guard_i, guard_j = next_gi, next_gj
            visited.add((guard_i, guard_j))
    
        print(f"{len(visited)=}")
    
    
    def part2(guard_i, guard_j):
        # keep track of visited positions
        visited = set()
        visited.add((guard_i, guard_j))
    
        dir_idx = 0 # current direction index
        loops = 0   # loops encountered
    
        # walk through the path
        while True:
            delta_i, delta_j = DIRECTIONS[dir_idx]
            next_gi, next_gj = guard_i + delta_i, guard_j + delta_j # next pos
            
            # if out of bounds, we are done
            if not (0 <= next_gi < m) or not (0 <= next_gj < n):
                break
            # change direction when obstacle encountered
            if rows[next_gi][next_gj] == "#":
                dir_idx = (dir_idx + 1) % 4
                continue
            # if a position is not already in path,
            # put a obstacle there and see if guard will loop
            if (next_gi, next_gj) not in visited and willLoop(guard_i, guard_j, dir_idx):
                loops += 1
            # update position and visited
            guard_i, guard_j = next_gi, next_gj
            visited.add((guard_i, guard_j))
        
        print(f"{loops=}")
    
    # used in part 2
    # returns whether placing an obstacle on next pos causes a loop or not
    def willLoop(guard_i, guard_j, dir_idx) -> bool:
        # obstacle pos
        obs_i, obs_j = guard_i + DIRECTIONS[dir_idx][0], guard_j + DIRECTIONS[dir_idx][1]
    
        # keep track of visited pos and the direction of travel
        visited: defaultdict[tuple[int, int], list[int]] = defaultdict(list)
        visited[(guard_i, guard_j)].append(dir_idx)
        
        # walk until guard exits map or loops
        while True:
            delta_i, delta_j = DIRECTIONS[dir_idx]
            next_gi, next_gj = guard_i + delta_i, guard_j + delta_j # next pos
            
            # if out of bounds, no loop
            if not (0 <= next_gi < m) or not (0 <= next_gj < n):
                return False
            # change direction when obstacle encountered
            if rows[next_gi][next_gj] == "#" or (next_gi == obs_i and next_gj == obs_j):
                dir_idx = (dir_idx + 1) % 4
                continue
            # we are looping if we encounter a visited pos in a visited direction
            if (next_gi, next_gj) in visited and dir_idx in visited[(next_gi, next_gj)]:
                return True
            
            # update position and visited
            guard_i, guard_j = next_gi, next_gj        
            visited[(guard_i, guard_j)].append(dir_idx)
    
    
    part1(guard_i, guard_j)
    part2(guard_i, guard_j)
    
    
      • Deebster@programming.dev
        link
        fedilink
        arrow-up
        3
        ·
        edit-2
        5 days ago

        My rust code ran in 6s on my phone (Samsung A35 running under Termux). When I’m back at a computer it’d be interesting to compare times properly.

        • CameronDev@programming.devOPM
          link
          fedilink
          arrow-up
          1
          ·
          edit-2
          5 days ago

          I got mine down to 3s, but it wasn’t a very smart loop detection. All I did was count steps and stop after 10000. The 9 second run was 100000 steps, which is obviously a bit excessive.

          Does save iterating over the list of past visits, so probably a good shortcut.

        • CameronDev@programming.devOPM
          link
          fedilink
          arrow-up
          2
          ·
          5 days ago

          How did you detect loops? I just ran for 100000 steps to see if I escaped, got my time down to 3s by doing only 10000 steps.

          • TunaCowboy@lemmy.world
            link
            fedilink
            arrow-up
            2
            ·
            edit-2
            5 days ago

            I added each visited position/direction to a set, and when a ‘state’ is reached again you have entered a loop:

            v = set()
            while t[g.r][g.c] != 'X':
                state = (g.r, g.c, g.d)
                if state in v:
                    acc += 1
                    break
                v.add(state)
                g.move(t)
            

            You can view my full solution here.

          • Leavingoldhabits@lemmy.world
            link
            fedilink
            arrow-up
            2
            ·
            5 days ago

            Not who you asked but: I save coordinates and direction into a vector each time the guard faces a #. Also every time the guard faces a #, I check if the position exists in the vector, if true, it’s an infinite loop. 78ms rust aolution.

            • CameronDev@programming.devOPM
              link
              fedilink
              arrow-up
              1
              ·
              5 days ago

              That’s probably quite optimal, compared with checking every state in the path, or running off a fixed number of steps

      • iAvicenna@lemmy.world
        link
        fedilink
        arrow-up
        1
        ·
        edit-2
        5 days ago

        I did a similar approach (place obstacles on guards path). Takes about 80s 10-15s in 11th Gen Intel® Core™ i7-11800H. Motivated by the code above, I also restricted the search to start right before the obstacle rather than the whole path which took it down from 80s to 10-15s