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

4 points

C#

public class Day06 : Solver
{
  private readonly (int, int)[] directions = [
    (0, -1), (1, 0), (0, 1), (-1, 0)
    ];

  private ImmutableArray<string> data;
  private int width, height;
  private ImmutableHashSet<(int, int)> guard_path;
  private int start_x, start_y;

  public void Presolve(string input) {
    data = input.Trim().Split("\n").ToImmutableArray();
    width = data[0].Length;
    height = data.Length;
    for (int i = 0; i < width; i++) {
      for (int j = 0; j < height; j++) {
        if (data[j][i] == '^') {
          start_x = i;
          start_y = j;
          break;
        }
      }
    }
    guard_path = Walk().Path.ToImmutableHashSet();
  }

  private bool IsWithinBounds(int x, int y) => x >= 0 && y >= 0 && x < width && y < height;
  
  private (HashSet<(int, int)> Path, bool IsLoop) Walk((int, int)? obstacle = null) {
    int obstacle_x = obstacle?.Item1 ?? -1;
    int obstacle_y = obstacle?.Item2 ?? -1;
    int direction = 0;
    int x = start_x;
    int y = start_y;
    bool loop = false;
    HashSet<(int, int, int)> positions = new();
    while (IsWithinBounds(x, y)) {
      if (positions.Contains((x, y, direction))) {
        loop = true;
        break;
      }
      positions.Add((x, y, direction));
      int nx = x + directions[direction].Item1;
      int ny = y + directions[direction].Item2;

      while (IsWithinBounds(nx, ny) && (data[ny][nx] == '#' || (nx == obstacle_x && ny == obstacle_y))) {
        direction = (direction + 1) % 4;
        nx = x + directions[direction].Item1;
        ny = y + directions[direction].Item2;
      }
      x = nx;
      y = ny;
    }
    return (positions.Select(position => (position.Item1, position.Item2)).ToHashSet(), loop);
  }

  public string SolveFirst() => guard_path.Count.ToString();
  public string SolveSecond() => guard_path
    .Where(position => position != (start_x, start_y))
    .Where(position => Walk(position).IsLoop)
    .Count().ToString();
}
permalink
report
reply
5 points
*

Haskell

This was a fun one! Infinite loops, performance concerns and so on. Part 2 could be made a bit faster with a recursive approach (I think), but this is good enough for me. Lost quite a bit of time with an incorrect walk function that passed the test data and part 1 but not part 2.

import Data.Array.Unboxed (UArray)
import Data.Array.Unboxed qualified as Array
import Data.List
import Data.Maybe
import Data.Set (Set)
import Data.Set qualified as Set

readInput :: String -> UArray (Int, Int) Char
readInput s =
  let rows = lines s
   in Array.listArray ((1, 1), (length rows, length $ head rows)) $ concat rows

startPos = fst . fromJust . find ((== '^') . snd) . Array.assocs

walk grid = go (startPos grid) (-1, 0)
  where
    go pos@(i, j) dir@(di, dj) =
      (pos, dir)
        : let pos' = (i + di, j + dj)
           in if Array.inRange (Array.bounds grid) pos'
                then case grid Array.! pos' of
                  '#' -> go pos (dj, -di)
                  _ -> go pos' dir
                else []

path = Set.fromList . map fst . walk

part1 = Set.size . path

part2 grid = Set.size $ Set.filter (isLoop . walk . addO) $ Set.delete (startPos grid) $ path grid
  where
    addO pos = grid Array.// [(pos, '#')]
    isLoop xs = or $ zipWith Set.member xs $ scanl' (flip Set.insert) Set.empty xs

main = do
  input <- readInput <$> readFile "input06"
  print $ part1 input
  print $ part2 input
permalink
report
reply
4 points
*

Haskell

This one was fun, I think I wrote my first lazy infinite loop I cancel out at runtime, lost some time because I had misread the requirements on turning right.

Runs in 45 seconds on my Laptop in power-saver mode, which isnโ€™t very fast I fear.

import Control.Arrow hiding (first, second)

import Data.Map (Map)
import Data.Set (Set)
import Data.Bifunctor

import qualified Data.Array.Unboxed as Array
import qualified Data.List as List
import qualified Data.Set as Set
import Data.Array.Unboxed (UArray)

parse :: String -> (UArray (Int, Int) Char, (Int, Int))
parse s = (a, p)
        where
                p = Array.indices 
                        >>> filter ((a Array.!) >>> (== '^'))
                        >>> head
                        $ a
                a = Array.listArray ((1, 1), (n, m)) . filter (/= '\n') $ s
                l = lines s
                (n, m) = (length . head &&& pred . length) l

rotate90 d@(-1, 0) = (0, 1)
rotate90 d@(0,  1) = (1, 0)
rotate90 d@(1,  0) = (0, -1)
rotate90 d@(0, -1) = (-1, 0)

walkGuard :: (UArray (Int, Int) Char) -> (Int, Int) -> (Int, Int) -> [((Int, Int), (Int, Int))]
walkGuard a p d@(dy, dx)
        | not isInBounds                  = []
        | (maybe ' ' id tileAhead) == '#' = (p, d) : walkGuard a p rotatedDirection
        | otherwise                       = (p, d) : walkGuard a updatedPosition d
        where
                isInBounds = Array.inRange (Array.bounds a) p
                updatedPosition = bimap (+dy) (+dx) p
                tileAhead = a Array.!? updatedPosition
                rotatedDirection = rotate90 d

ruleGroup :: Eq a => (a, b) -> (a, b') -> Bool
ruleGroup = curry (uncurry (==) <<< fst *** fst)
                
arrayDisplay a = Array.indices
        >>> List.groupBy ruleGroup
        >>> map (map (a Array.!))
        >>> unlines
        $ a

walkedPositions a p d = walkGuard a p 
        >>> map fst
        >>> Set.fromList
        $ d

isLoop = isLoop' Set.empty
isLoop' _ [] = False
isLoop' s (l:ls)
        | l `Set.member` s = True
        | otherwise = isLoop' (Set.insert l s) ls

part1 (a, p) = walkedPositions a p
        >>> length
        $ (-1, 0)
part2 (a, p) = walkedPositions a p
        >>> Set.toList
        >>> map (, '#')
        >>> map (:[])
        >>> map (a Array.//)
        >>> map (\ a' -> walkGuard a' p (-1, 0))
        >>> filter (isLoop)
        >>> length
        $ (-1, 0)

main = getContents >>= print . (part1 &&& part2) . parse
permalink
report
reply
3 points

TypeScript

The code
import fs from "fs";

enum GuardDirection {
    UP = "^",
    RIGHT = ">",
    DOWN = "v",
    LEFT = "<",
};

const originalGrid: string[][] = fs.readFileSync("./06/input.txt", "utf-8")
    .split(/[\r\n]+/)
    .filter(Boolean)
    .map(row => row.split(""))
    .map(row => row.map(char => char === "." ? "" : char));

const gridWidth = originalGrid[0].length;
const startingPosition = getStartPosition(originalGrid);
const startingDirection = originalGrid[startingPosition.y][startingPosition.x] as GuardDirection;

originalGrid[startingPosition.y][startingPosition.x] = "";

// Part 1
const grid = getGridCopy(originalGrid);
doGuardMovement(grid);
console.info("Part 1: " + grid.flatMap(row => row).filter(cell => cell.startsWith("X")).length);

// Part 2
let part2Result = 0;
for (let y = 0; y < originalGrid.length; y++) {
    for (let x = 0; x < originalGrid.length; x++) {
        if (!originalGrid[y][x].length && grid[y][x].startsWith("X")) {
            // Cell is empty AND was visited during part 1 => Should place an obstacle here
            const gridCopy = getGridCopy(originalGrid);
            gridCopy[y][x] = "#";
            if (!doGuardMovement(gridCopy)) { part2Result++; }
        }
    }
}
console.info("Part 2: " + part2Result);

function doGuardMovement(grid: string[][]): boolean { // Returns false if loop detected
    let [x, y, guard] = [startingPosition.x, startingPosition.y, startingDirection];

    while (y >= 0 && y < grid.length && x >= 0 && x < gridWidth) {
        // Check for loop
        if (grid[y][x].length > 3) { return false; }
        grid[y][x] += "X"; // Mark each visitation with X
        
        // If there is something directly in front of you, turn right 90 degrees
        if (guard === GuardDirection.UP && y > 0 && grid[y - 1][x] === "#") { guard = GuardDirection.RIGHT; }
        else if (guard === GuardDirection.RIGHT && x < gridWidth - 1 && grid[y][x + 1] === "#") { guard = GuardDirection.DOWN; }
        else if (guard === GuardDirection.DOWN && y < grid.length - 1 && grid[y + 1][x] === "#") { guard = GuardDirection.LEFT; }
        else if (guard === GuardDirection.LEFT && x > 0 && grid[y][x - 1] === "#") { guard = GuardDirection.UP; }
    
        // Otherwise, take a step forward
        else if (guard === GuardDirection.UP) { y--; }
        else if (guard === GuardDirection.RIGHT) { x++; }
        else if (guard === GuardDirection.DOWN) { y++; }
        else if (guard === GuardDirection.LEFT) { x--; }
        
        else { throw new Error("Something went wrong"); }
    }

    return true; // Exited the grid
}

function getGridCopy(grid: string[][]): string[][] {
    return grid.map(row => [...row]);
}

function getStartPosition(grid: string[][]): {x: number, y: number} {
    for (let y = 0; y < grid.length; y++) {
        for (let x = 0; x < grid.length; x++) {
            if (Object.values(GuardDirection).some(char => grid[y][x] === char)) {
                return {x, y};
            }
        }
    }

    throw new Error("Could not find starting position");
}
permalink
report
reply
3 points
*

Rust

Only part 1 because Iโ€™m meant to be leaving for a holiday in a few hours and havenโ€™t packed yet. Part two looks simple enough to add:

part 2 plan

Change seen positions set to include direction, if pos+dir already seen then itโ€™s a loop. Test all spaces.

Edit: I did the change on my phone (which was painful).

use std::{collections::HashSet, fs, str::FromStr};

use color_eyre::eyre::{Report, Result};

type GridPosition = usize;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Direction {
    N,
    E,
    S,
    W,
}

impl Direction {
    fn clockwise(&self) -> Self {
        match self {
            Direction::N => Direction::E,
            Direction::E => Direction::S,
            Direction::S => Direction::W,
            Direction::W => Direction::N,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Thing {
    Guard(Direction),
    Obstruction,
    Space,
}

#[derive(Debug, PartialEq, Eq)]
struct LabMap {
    grid: Vec<Thing>,
    width: usize,
    height: usize,
}

impl FromStr for LabMap {
    type Err = Report;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let grid = s
            .chars()
            .filter_map(|ch| {
                use Thing::*;
                match ch {
                    '^' => Some(Guard(Direction::N)),
                    '>' => Some(Guard(Direction::E)),
                    'v' => Some(Guard(Direction::S)),
                    '<' => Some(Guard(Direction::W)),
                    '#' => Some(Obstruction),
                    '.' => Some(Space),
                    '\n' => None,
                    _ => unreachable!(),
                }
            })
            .collect::<Vec<_>>();
        let width = s
            .chars()
            .position(|ch| ch == '\n')
            .ok_or_else(|| Report::msg("grid width cannot be zero, or one line"))?;
        let height = grid.len() / width;
        Ok(Self {
            grid,
            width,
            height,
        })
    }
}

impl LabMap {
    fn neighbour(&self, i: GridPosition, dir: Direction) -> Option<GridPosition> {
        let width = self.width;
        let length = self.grid.len();
        use Direction::*;
        match dir {
            N if i >= width => Some(i - width),
            E if i % width != width - 1 => Some(i + 1),
            S if i + width < length => Some(i + width),
            W if i % width != 0 => Some(i - 1),
            _ => None,
        }
    }

    fn guard_pos(&self) -> Option<(GridPosition, Direction)> {
        self.grid
            .iter()
            .enumerate()
            .filter_map(|(pos, &thing)| match thing {
                Thing::Guard(dir) => Some((pos, dir)),
                _ => None,
            })
            .next()
    }

    fn path_len(&self) -> usize {
        let mut positions = HashSet::new();
        let mut next = self.guard_pos();
        while let Some((pos, dir)) = next {
            positions.insert(pos);

            next = self.neighbour(pos, dir).map(|npos| match self.grid[npos] {
                Thing::Space | Thing::Guard(_) => (npos, dir),
                Thing::Obstruction => (pos, dir.clockwise()),
            });
        }
        positions.len()
    }

    fn num_loops(&self) -> usize {
        (0..self.grid.len())
            .filter(|&pos| matches!(self.grid[pos], Thing::Space))
            .map(|pos| {
                let mut grid = self.grid.clone();
                grid[pos] = Thing::Obstruction;
                LabMap {
                    grid,
                    width: self.width,
                    height: self.height,
                }
            })
            .filter(LabMap::is_loop)
            .count()
    }

    fn is_loop(&self) -> bool {
        let mut positions = HashSet::new();
        let mut next = self.guard_pos();
        while let Some((pos, dir)) = next {
            let is_new = positions.insert((pos, dir));
            if !is_new {
                return true;
            }

            next = self.neighbour(pos, dir).map(|npos| match self.grid[npos] {
                Thing::Space | Thing::Guard(_) => (npos, dir),
                Thing::Obstruction => (pos, dir.clockwise()),
            });
        }
        false
    }
}

fn part1(filepath: &str) -> Result<usize> {
    let input = fs::read_to_string(filepath)?;
    let map = LabMap::from_str(&input)?;
    Ok(map.path_len())
}

fn part2(filepath: &str) -> Result<usize> {
    let input = fs::read_to_string(filepath)?;
    let map = LabMap::from_str(&input)?;
    Ok(map.num_loops())
}

fn main() -> Result<()> {
    color_eyre::install()?;

    println!("Part 1: {}", part1("input.txt")?);
    println!("Part 2: {}", part2("input.txt")?);
    Ok(())
}

permalink
report
reply

Advent Of Code

!advent_of_code@programming.dev

Create post

An unofficial home for the advent of code community on programming.dev!

Advent of Code is an annual Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

AoC 2024

Solution Threads

M T W T F S S
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25

Rules/Guidelines

  • Follow the programming.dev instance rules
  • Keep all content related to advent of code in some way
  • If what youre posting relates to a day, put in brackets the year and then day number in front of the post title (e.g. [2024 Day 10])
  • When an event is running, keep solutions in the solution megathread to avoid the community getting spammed with posts

Relevant Communities

Relevant Links

Credits

Icon base by Lorc under CC BY 3.0 with modifications to add a gradient

console.log('Hello World')

Community stats

  • 467

    Monthly active users

  • 111

    Posts

  • 1.1K

    Comments