this post was submitted on 06 Dec 2024
26 points (96.4% liked)

Advent Of Code

979 readers
29 users here now

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

Relevant Communities

Relevant Links

Credits

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

console.log('Hello World')

founded 1 year ago
MODERATORS
 

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

all 42 comments
sorted by: hot top controversial new old
[–] [email protected] 5 points 2 weeks ago* (last edited 2 weeks ago)

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
[–] [email protected] 4 points 2 weeks ago

Not a big fan of this one, felt far too much like brute force for my liking.
At least it works with AsParallel...

C#

public struct Point : IEquatable<Point>
{
  public int X { get; set; }
  public int Y { get; set; }

  public Point(int x = 0, int y = 0) {
    X = x;
    Y = y;
  }

  public static Point operator+(Point l, Point r) {
    return new Point(l.X + r.X, l.Y + r.Y);
  }
  public bool Equals(Point other) {
    return X == other.X && Y == other.Y;
  }
}

Point size = new Point(0, 0);
HashSet<Point> obstructions = new HashSet<Point>();
Point guard = new Point(0, 0);

enum Direction
{
  Up = 0,
  Right,
  Down,
  Left
}

public void Input(IEnumerable<string> lines)
{
  size = new Point(lines.First().Length, lines.Count());
  char[] map = string.Join("", lines).ToCharArray();

  for (int y = 0; y < size.Y; ++y)
    for (int x = 0; x < size.X; ++x)
    {
      int pos = y * size.X + x;
      char at = map[pos];
      if (at == '#')
        obstructions.Add(new Point(x, y));
      else if (at == '^')
        guard = new Point(x, y);
    }
}

List<Point> path = new List<Point>();
public void PreCalc()
{
  path = WalkArea().path.Distinct().ToList();
}

public void Part1()
{
  Console.WriteLine($"Visited {path.Count} points");
}
public void Part2()
{
  int loopPoints = path.AsParallel().Where(p => !p.Equals(guard) && WalkArea(p).loop).Count();
  Console.WriteLine($"Valid loop points: {loopPoints}");
}

(IEnumerable<Point> path, bool loop) WalkArea(Point? obstruction = null)
{
  HashSet<(Point, Direction)> loopDetect = new HashSet<(Point, Direction)>();

  Point at = guard;
  Direction dir = Direction.Up;
  while (true)
  {
    if (!loopDetect.Add((at, dir)))
      return (loopDetect.Select(p => p.Item1), true);

    Point next = at;
    switch(dir)
    {
    case Direction.Up: next += new Point(0, -1); break;
    case Direction.Right: next += new Point(1, 0); break;
    case Direction.Down: next += new Point(0, 1); break;
    case Direction.Left: next += new Point(-1, 0); break;
    }

    if (next.X < 0 || next.Y < 0 || next.X >= size.X || next.Y >= size.Y)
      break;
    else if (obstructions.Contains(next) || (obstruction?.Equals(next) ?? false))
      dir = (Direction)(((int)dir + 1) % 4);
    else
      at = next;
  }

  return (loopDetect.Select(p => p.Item1), false);
}

[–] Pyro 4 points 2 weeks ago* (last edited 2 weeks ago) (1 children)

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)

[–] CameronDev 3 points 2 weeks ago (4 children)

How long did brute force take? Mine was 9s on an m1 with rust.

[–] Deebster 3 points 2 weeks ago* (last edited 2 weeks 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.

[–] Pyro 2 points 2 weeks ago* (last edited 2 weeks ago) (1 children)

About 15-20 seconds, not too bad.

[–] CameronDev 1 points 2 weeks ago* (last edited 2 weeks 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.

[–] [email protected] 1 points 2 weeks ago* (last edited 2 weeks ago)

I did a similar approach (place obstacles on guards path). Takes about ~~80s~~ 10-15s in 11th Gen Intel(R) Core(TM) 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

[–] [email protected] 1 points 2 weeks ago (1 children)

Mine was 9s

That's about how long it takes for my python solution to complete.

[–] CameronDev 2 points 2 weeks ago (2 children)

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.

[–] [email protected] 2 points 2 weeks ago* (last edited 2 weeks 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.

[–] [email protected] 2 points 2 weeks ago (1 children)

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 1 points 2 weeks ago

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

[–] [email protected] 4 points 2 weeks ago

Lisp

Brute forced part 2, but got a lot of reuse from part 1.

Part 1 and 2


(defvar *part1* "inputs/day06-part1")
(defvar *part1-test* "inputs/day06-part1-test")

(defstruct move x y direction)

(defstruct guard direction x y (moves (make-hash-table :test 'equalp)))

(defun convert-direction (g)
  (case g
    (^ 'up)
    (> 'right)
    (< 'left)
    (v 'down)))


(defun find-guard (map)
  (destructuring-bind (rows cols) (array-dimensions map)
    (loop for j from 0 below rows
          do (loop for i from 0 below cols
                   for v = (aref  map j i)
                   when (not (or (eql '|.| v) (eql '|#| v)))
                     do (return-from find-guard (make-guard :direction (convert-direction v) :x i :y j ))))))

(defun turn-guard (guard)
  (case (guard-direction guard)
    (UP (setf (guard-direction guard) 'RIGHT))
    (DOWN (setf (guard-direction guard) 'LEFT))
    (LEFT (setf (guard-direction guard) 'UP))
    (RIGHT (setf (guard-direction guard) 'DOWN))))

(defun on-map (map x y)
  (destructuring-bind (rows cols) (array-dimensions map)
    (and (>= x 0) (>= y 0)
         (< y rows) (< x cols))))

(defun mark-guard (map guard)
  (setf (aref map (guard-y guard) (guard-x guard)) 'X))

(defun next-pos (guard)
  (case (guard-direction guard)
    (UP (list (guard-x guard) (1- (guard-y guard))))
    (DOWN (list (guard-x guard) (1+ (guard-y guard))))
    (LEFT (list (1- (guard-x guard)) (guard-y guard)))
    (RIGHT (list (1+ (guard-x guard)) (guard-y guard)))))

(defun move-guard (map guard)
  (destructuring-bind (x y) (next-pos guard)
    (if (on-map map x y)
        (if (eql '|#| (aref map y x))
            (turn-guard guard)
            (progn (setf (guard-x guard) x)
                   (setf (guard-y guard) y))) 
        (setf (guard-direction guard) nil))))

(defun run-p1 (file) 
  (let* ((map (list-to-2d-array (read-file file #'to-symbols)))
         (guard (find-guard map)))
    (mark-guard map guard)
    (loop while (guard-direction guard)
          do (mark-guard map guard)
          do (move-guard map guard))
    (destructuring-bind (rows cols) (array-dimensions map)
      (loop for y from 0 below rows sum (loop for x from 0 below cols count (eql (aref map y x) 'X))))))

(defun save-move (guard move)
  (setf (gethash move (guard-moves guard)) t))

(defun reset-moves (guard)
  (setf (guard-moves guard) nil))

(defun is-loop (x y map original-guard)
  ;; can only set new blocks in blank spaces
  (unless (eql '|.| (aref map y x)) (return-from is-loop nil))
  (let ((guard (copy-guard original-guard)))
    ;; save the initial guard position
    (save-move guard (make-move :x (guard-x guard) :y (guard-y guard) :direction (guard-direction guard)))
    ;; set the "new" block
    (setf (aref map y x) '|#|)
    ;; loop and check for guard loops
    (let ((result
            (loop
              while (move-guard map guard)
              for move = (make-move :x (guard-x guard) :y (guard-y guard) :direction (guard-direction guard))
              ;; if we have seen the move before, then it is a loop
              if (gethash move (guard-moves guard))
                return t
              else
                do (save-move guard move)
              finally
                 (return nil))))

      ;; reset initial position
      (setf (aref map y x) '|.|)
      (clrhash (guard-moves guard))
      result)))


(defun run-p2 (file) 
  (let* ((map (list-to-2d-array (read-file file #'to-symbols)))
         (guard (find-guard map)))
    
    (destructuring-bind (rows cols) (array-dimensions map)
      (loop for y from 0 below rows
            sum (loop for x from 0 below cols
                      count (is-loop x y map guard)))
      )))

[–] [email protected] 4 points 2 weeks ago

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();
}
[–] Gobbel2000 4 points 2 weeks ago

Rust

In part 2 it took me some time to figure out that I cannot immediately move after turning, but then it worked fairly well. As a slight optimization I check only the places that were visited without obstacles (the solution from part 1). With this, part 2 takes 52ms.

Solution

use euclid::default::{Point2D, Vector2D};
use euclid::vec2;

fn parse(input: String) -> (Vec<Vec<bool>>, Point2D<i32>) {
    let mut field = Vec::new();
    let mut start = Point2D::zero();
    for (y, line) in input.lines().enumerate() {
        let mut row = Vec::new();
        for (x, c) in line.chars().enumerate() {
            row.push(c == '#');
            if c == '^' {
                start = Point2D::new(x, y).to_i32();
            }
        }
        field.push(row);
    }
    (field, start)
}

const DIRS: [Vector2D<i32>; 4] = [vec2(0, -1), vec2(1, 0), vec2(0, 1), vec2(-1, 0)];

fn visited(field: &[Vec<bool>], start: Point2D<i32>) -> Vec<Vec<bool>> {
    let width = field[0].len();
    let height = field.len();
    let mut visited = vec![vec![false; width]; height];
    // Start up, then turn right
    let mut dir = 0;
    let mut pos = start;
    loop {
        visited[pos.y as usize][pos.x as usize] = true;
        let next = pos + DIRS[dir];
        // Guard leaves area
        if next.x < 0 || next.y < 0 || next.x >= width as i32 || next.y >= height as i32 {
            break;
        }
        // Path blocked
        if field[next.y as usize][next.x as usize] {
            dir = (dir + 1) % 4; // Turn right, don't move yet
        } else {
            pos = next
        }
    }
    visited
}

fn part1(input: String) {
    let (field, start) = parse(input);
    let count = visited(&field, start)
        .iter()
        .map(|r| r.iter().map(|b| u32::from(*b)).sum::<u32>())
        .sum::<u32>();
    println!("{count}")
}

fn is_loop(field: &[Vec<bool>], start: Point2D<i32>) -> bool {
    let width = field[0].len();
    let height = field.len();
    let mut visited = vec![vec![0; width]; height];

    // Start up, then turn right
    let mut dir = 0;
    let mut pos = start;
    loop {
        // Loop detected
        if visited[pos.y as usize][pos.x as usize] & (1 << dir) > 0 {
            break true;
        }
        // Record all walked directions at all fields
        visited[pos.y as usize][pos.x as usize] |= 1 << dir;
        let next = pos + DIRS[dir];
        // Guard leaves area
        if next.x < 0 || next.y < 0 || next.x >= width as i32 || next.y >= height as i32 {
            break false;
        }
        // Path blocked
        if field[next.y as usize][next.x as usize] {
            dir = (dir + 1) % 4 // Turn right, don't move yet
        } else {
            pos = next
        }
    }
}

fn part2(input: String) {
    let (mut field, start) = parse(input);
    let width = field[0].len();
    let height = field.len();
    let normal_visited = visited(&field, start); // Part 1 solution
    let mut count = 0;
    for x in 0..width {
        for y in 0..height {
            // Only check places that are visited without any obstacles, and don't check start
            if normal_visited[y][x] && !(x as i32 == start.x && y as i32 == start.y) {
                field[y][x] = true; // Set obstacle
                count += is_loop(&field, start) as u32;
                field[y][x] = false; // Remove obstacle
            }
        }
    }
    println!("{count}");
}

util::aoc_main!();

also on github

[–] [email protected] 4 points 2 weeks ago* (last edited 2 weeks ago)

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
[–] [email protected] 3 points 2 weeks ago* (last edited 2 weeks ago) (1 children)

Rust

This one was the first real think for this year, but I ended up brute forcing it, placing a β€˜#’ in every position and checking. part 2 runs in about ~~380ms~~ 78ms (after reducing the amount β€˜#’-placements to only where the guard walks) on my 2011 core i-7, so I’m happy, even though it feels like I could have been smarter.

Part 1 Part 2

[–] [email protected] 2 points 2 weeks ago* (last edited 2 weeks ago) (4 children)

I am doing the same principle brute force but it takes ~7 seconds oO

Is using a HashSet<(Pos, Dir)> for the loop detection so expensive? My CPU shouldn't be THAT bad..

Part one around 7ms.

Also curious that i have not seen someone mention a more efficient approach, there gotta be one?

[–] [email protected] 3 points 2 weeks ago* (last edited 2 weeks ago) (1 children)

I draw ^>v< characters on the grid while walking, so then it's a direct array lookup (instead of a hashtable). The representation could be better though, some kind of bitmask would beat checking against a bunch of characters.

[–] [email protected] 1 points 2 weeks ago (1 children)

I dont change the map, i just record the steps in the hashtable. But maybe drawing on the map is indeed shaving some time off, thanks for the input :)

[–] [email protected] 1 points 2 weeks ago

It probably won’t matter a whole deal but array indexing involves no comparisons or searches. And I found it convenient too!

[–] [email protected] 2 points 2 weeks ago* (last edited 2 weeks ago) (1 children)

I’d like to see your solution in total. I’m not too familiar with the nuts and bolts, but hash set is quite a bit more expensive than a simple vector, there’s a bunch of overhead incurred when executing the hashing and placing of the data, and when repeating a few thousand times it sure adds up. My part one hovers around 600 microseconds.

[–] [email protected] 2 points 2 weeks ago

I’d like to see your solution in total.

I set it up a bit like a game, https://pastebin.com/FGA6E7fA

My part one hovers around 600 microseconds.

Ohhh, that says my part 1 is slow already, i was sure my approach for 2 was the problem. Good to know!

[–] [email protected] 2 points 2 weeks ago* (last edited 2 weeks ago) (1 children)

I created rows and cols vecs that keep places of blocks. When moving, I binary search the row or col, find the block that stops me. So moving whole sections at once. Otherwise used HashSet of pos and dir like you. Also in part 2, place the new block only on the path I take in part1. Part 2 is 26ms.

code

[–] [email protected] 2 points 2 weeks ago

The binary search sounds smart, would reduce the pathing quite a bit i guess :)

Part 2 i approached quite the same i think, was only a couple lines of code additionally. But running 5ms 5000 times is also gonna take a while..

[–] [email protected] 1 points 2 weeks ago

Alright, I completely forgot about --release because i normally use just to run my stuff. That brings part 2 down to around 400ms, i am okay with that for now :D

[–] Deebster 3 points 2 weeks ago* (last edited 2 weeks ago)

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 planChange 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(())
}

[–] [email protected] 3 points 2 weeks ago

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");
}

[–] [email protected] 2 points 2 weeks ago

C

Got super stumped on part 2. I'd add an obstacle for every tile on the path of part 1 but I kept getting wrong results, even after fixing some edge cases. Spent too much time looking at terminal dumps and mp4 visualisations.

Eventually I gave up and wrote a for(y) for(x) loop, trying an obstacle in every possible tile, and that gave the correct answer. Even that brute force took only 2.5 ish seconds on my 2015 PC! But having that solution allowed me to narrow it down again to a reasonably efficient version similar to what I had before. Still I don't know where I went wrong the first time.

Code

#include "common.h"

#define GZ 134

struct world { char g[GZ][GZ]; int x,y, dir; };

static const char carets[] = "^>v<";
static const int dx[4] = {0,1,0,-1}, dy[4] = {-1,0,1,0};

static inline char *ahead(struct world *w) {
    return &w->g[w->y+dy[w->dir]][w->x+dx[w->dir]]; }
static inline int visited(char t) { return t && strchr(carets, t); }
static inline int traversible(char t) { return t=='.' || visited(t); }

/* new tile, previously visited tile, in a loop, out of bounds */
enum { ST_NEW, ST_SEEN, ST_LOOP, ST_END };

static int
step(struct world *w)
{
	char *cell;
	int is_new;

	assert(w->x >= 0); assert(w->x < GZ);
	assert(w->y >= 0); assert(w->y < GZ);

	cell = &w->g[w->y][w->x];

	if (!traversible(*cell))	/* out of bounds? */
		return ST_END;
	while (*ahead(w) == '#')	/* turn if needed */
		w->dir = (w->dir +1) %4;
	if (*cell == carets[w->dir])	/* walked here same dir? */
		return ST_LOOP;

	is_new = *cell == '.';
	*cell = carets[w->dir];
	w->x += dx[w->dir];
	w->y += dy[w->dir];

	return is_new ? ST_NEW : ST_SEEN;
}

int
main(int argc, char **argv)
{
	static struct world w0,w1,w2;
	int p1=0,p2=0, x,y, r,i;

	if (argc > 1)
		DISCARD(freopen(argv[1], "r", stdin));

	for (y=1; y<GZ && fgets(w0.g[y]+1, GZ-1, stdin); y++)
		;

	for (y=0; y<GZ; y++)
	for (x=0; x<GZ; x++)
	for (i=0; i<4; i++)
		if (w0.g[y][x] == carets[i])
			{ w0.x=x; w0.y=y; w0.dir=i; goto found_start; }
found_start:
	w0.g[y][x] = '.';

	/* keep the clean copy of the grid (needed for part 2) */
	memcpy(&w1, &w0, sizeof(w1));

	/* part 1: trace the path and count unseen tiles */
	while ((r = step(&w1)) <= ST_SEEN)
		p1 += r == ST_NEW;

	/* part 2: try putting obstacles on each tile seen in p1 */
	for (y=0; y<GZ; y++)
	for (x=0; x<GZ; x++)
		if (visited(w1.g[y][x])) {
			memcpy(&w2, &w0, sizeof(w2));
			w2.g[y][x] = '#';
			while ((r = step(&w2)) <= ST_SEEN) ;
			p2 += r == ST_LOOP;
		}

	printf("06: %d %d\n", p1, p2);
	return 0;
}

https://github.com/sjmulder/aoc/blob/master/2024/c/day06.c

[–] [email protected] 2 points 2 weeks ago* (last edited 2 weeks ago)

J

Today's the first one where I feel like the choice of language is a disadvantage without compensating advantages. Or, at least, I don't know J well enough yet to use its compensating advantages for this kind of task, so what I end up with is Python 2 with obscure syntax and no associative data structures.

Also, I can't post my code, because apparently Lemmy is interpreting some of today's bizarre line noise as hostile and sanitizing it. It looks more or less like the other imperative solutions here, just with more punctuation.

[–] [email protected] 2 points 2 weeks ago

TypeScript

Solution

import { AdventOfCodeSolutionFunction } from "./solutions";


export const check_coords = (grid: Grid, x: number, y: number) => {
    return y >= grid.length ||
        y < 0 ||
        x >= grid[y].length ||
        x < 0
}

export enum Direction {
    UP,
    UP_RIGHT,
    RIGHT,
    BOTTOM_RIGHT,
    BOTTOM,
    BOTTOM_LEFT,
    LEFT,
    UP_LEFT,
};

/**
 * This function should return true if it wants the search function to continue
 */
export type SearchFindFunction = (currChar: string, x: number, y: number) => boolean;

export type Grid = Array<Array<string>>;

export enum SearchExitReason {
    OUT_OF_BOUNDS,
    FUNCTION_FINISHED,
    INVALID_DIRECTION,
}

export const search_direction = (grid: Grid, x: number, y: number, direction: Direction, find: SearchFindFunction): SearchExitReason => {
    // invalid coords
    if (check_coords(grid, x, y))
        return SearchExitReason.OUT_OF_BOUNDS;

    // search failed
    if (!find(grid[y][x], x, y))
        return SearchExitReason.FUNCTION_FINISHED;

    switch (direction) {
        case Direction.UP:
            return search_direction(grid, x, y - 1, direction, find);

        case Direction.UP_RIGHT:
            return search_direction(grid, x + 1, y - 1, direction, find);

        case Direction.RIGHT:
            return search_direction(grid, x + 1, y, direction, find);

        case Direction.BOTTOM_RIGHT:
            return search_direction(grid, x + 1, y + 1, direction, find);

        case Direction.BOTTOM:
            return search_direction(grid, x, y + 1, direction, find);

        case Direction.BOTTOM_LEFT:
            return search_direction(grid, x - 1, y + 1, direction, find);

        case Direction.LEFT:
            return search_direction(grid, x - 1, y, direction, find);

        case Direction.UP_LEFT:
            return search_direction(grid, x - 1, y - 1, direction, find);

        default:
            return SearchExitReason.INVALID_DIRECTION;
    }
}

export const gridSearch = (grid: Grid, st: SearchFindFunction): [x: number, y: number] => {
    for (let y = 0; y < grid.length; y++) {
        const row = grid[y];
        for (let x = 0; x < row.length; x++) {
            const char = row[x];
            if (!st(char, x, y))
                return [x, y];
        }
    }

    return [-1, -1];
}

export const makeGridFromMultilineString =
    (input: string) => input.split("\n").map(st => st.trim()).map(v => v.split(""));

export const Duplicate2DArray = <T>(array: Array<Array<T>>) => {
    return [...array.map((item) => [...item])];
}



const NextDirection = (dir: Direction) => {
    switch (dir) {
        case Direction.UP:
            return Direction.RIGHT;
        case Direction.RIGHT:
            return Direction.BOTTOM;
        case Direction.BOTTOM:
            return Direction.LEFT;
        case Direction.LEFT:
            return Direction.UP;
        default:
            throw Error("Invalid direction");
    }
}

/**
 * @returns true if there are no loops
 */
const NoLoops = (grid: Grid, x: number, y: number, dir: Direction) => {
    const visited = new Set<string>();

    /**
     * @returns True if not visited yet
     */
    const addToVisited = (x: number, y: number, dir: Direction) => {
        const log = `${x}:${y}:${dir}`;

        if (visited.has(log))
            return false;

        visited.add(log);
        return true;
    }

    let searchResult: SearchExitReason = SearchExitReason.FUNCTION_FINISHED;
    let res = true;
    let i = 0; // rate limited for API
    let [lastX, lastY] = [x, y];
    while (searchResult !== SearchExitReason.OUT_OF_BOUNDS && i++ < 10_000) {
        searchResult = search_direction(grid, lastX, lastY, dir, (ch, currX, currY) => {
            if (ch == "#")
                return false;

            [lastX, lastY] = [currX, currY];

            res = addToVisited(currX, currY, dir);
            return res;
        });

        if (!res)
            break;

        dir = NextDirection(dir);
    }

    return res;
}


export const solution_6: AdventOfCodeSolutionFunction = (input) => {
    const grid = makeGridFromMultilineString(input);
    const visited = new Map<string, [x: number, y: number, dir: Direction, prevX: number, prevY: number]>();
    let [x, y] = gridSearch(grid, (ch) => ch !== "^");
    const [initialX, initialY] = [x, y];
    let dir: Direction = Direction.UP;

    const addToVisited = (visitedX: number, visitedY: number, dir: Direction) => {
        const loc = `${visitedX}:${visitedY}`;
        if (!visited.has(loc))
            visited.set(loc, [visitedX, visitedY, dir, x, y]);
    }

    addToVisited(x, y, dir);

    let res: SearchExitReason = SearchExitReason.FUNCTION_FINISHED;
    let i = 0; // rate limited for API
    while (res !== SearchExitReason.OUT_OF_BOUNDS && i++ < 10_000) {
        res = search_direction(grid, x, y, dir, (ch, currX, currY) => {
            if (ch == "#")
                return false;

            addToVisited(currX, currY, dir);
            [x, y] = [currX, currY];
            return true;
        });
        dir = NextDirection(dir);
    }

    const part_1 = visited.size;

    // remove starting position for part 1
    visited.delete(`${initialX}:${initialY}`);

    let part_2 = 0;
    visited.forEach((v) => {
        const [visitedX, visitedY, visitedDirection, prevX, prevY] = v;
        const newGrid = Duplicate2DArray(grid);
        newGrid[visitedY][visitedX] = "#"; // add a block

        // look for loops
        if (!NoLoops(newGrid, prevX, prevY, visitedDirection))
            part_2++;
    });

    return {
        part_1, // 4656
        part_2, // 1575
    }
}

I'm so surprised this runs in ~3s, expected it to take like 60s (do I have supercomputer?). Solution was similar to Pyro's in this thread as in it simulates the walk then places an obstacle in every possible position but I speed it up by starting just before the obstacle and looking towards it. Also, I reused some code from day 4 by tweaking it slightly. Thank you for the "S" tire Advent of Code puzzle. :)

[–] [email protected] 2 points 2 weeks ago* (last edited 2 weeks ago)

Nim

Not the prettiest code, but it runs in 3 seconds. For part 2 I just place an obstacle at every position guard visited in part 1.

Edit: made step procedure more readable.

type
  Vec2 = tuple[x,y: int]
  Dir = enum
    Up, Right, Down, Left
  Guard = object
    pos: Vec2
    dir: Dir

proc step(guard: var Guard, map: seq[string]): bool =
  let next: Vec2 =
    case guard.dir
    of Up: (guard.pos.x, guard.pos.y-1)
    of Right: (guard.pos.x+1, guard.pos.y)
    of Down: (guard.pos.x, guard.pos.y+1)
    of Left: (guard.pos.x-1, guard.pos.y)

  if next.y < 0 or next.x < 0 or next.y > map.high or next.x > map[0].high:
    return false
  elif map[next.y][next.x] == '#':
    guard.dir = Dir((guard.dir.ord + 1) mod 4)
  else:
    guard.pos = next
  true

proc solve(input: string): AOCSolution[int, int] =
  var map = input.splitLines()
  var guardStart: Vec2
  block findGuard:
    for y, line in map:
      for x, c in line:
        if c == '^':
          guardStart = (x, y)
          map[y][x] = '.'
          break findGuard

  var visited: HashSet[Vec2]
  block p1:
    var guard = Guard(pos: guardStart, dir: Up)
    while true:
      visited.incl guard.pos
      if not guard.step(map): break
    result.part1 = visited.len

  block p2:
    for (x, y) in visited - [guardStart].toHashSet:
      var loopCond: HashSet[Guard]
      var guard = Guard(pos: guardStart, dir: Up)
      var map = map
      map[y][x] = '#'

      while true:
        loopCond.incl guard
        if not guard.step(map): break
        if guard in loopCond:
          inc result.part2
          break

Codeberg repo

[–] proved_unglue 2 points 2 weeks ago

Kotlin

Not much inspiration. Brute forcing my way through today's level.

Solution

typealias Grid = List<List<Char>>

private val up: Char = '^'
private val down: Char = 'v'
private val left: Char = '<'
private val right: Char = '>'
private val obstacle: Char = '#'
private val directions: Set<Char> = setOf(up, down, left, right)

data class Position(
    var direction: Char,
    var row: Int,
    var col: Int,
    val visited: MutableSet<Pair<Int, Int>> = mutableSetOf(),
    val history: MutableSet<Triple<Char, Int, Int>> = mutableSetOf(),
) {
    override fun toString(): String = "Position(direction: $direction, position: ($row,$col))"
}

fun part1(input: String): Int {
    val grid: Grid = input.lines().map(String::toList)
    val position = findStartPosition(grid)
    while (!isEndPosition(position, grid)) {
        moveOrTurn(position, grid)
    }
    return position.visited.size
}

fun part2(input: String): Int {
    var loops = 0
    for (i in input.indices) {
        if (input[i] != '.') {
            continue
        }
        val sb = StringBuilder(input)
        sb.setCharAt(i, obstacle)
        val grid: Grid = sb.toString().lines().map(String::toList)
        val position = findStartPosition(grid)
        while (!isEndPosition(position, grid)) {
            moveOrTurn(position, grid)
            if (isLoop(position)) {
                loops++
                break
            }
        }
    }
    return loops
}

private fun findStartPosition(grid: Grid): Position {
    for (row in grid.indices) {
        for (col in grid[row].indices) {
            val c = grid[row][col]
            if (directions.contains(c)) {
                val pos = Position(c, row, col)
                pos.visited.add(Pair(row, col))
                return pos
            }
        }
    }
    throw IllegalStateException("No start position found")
}

private fun isEndPosition(position: Position, grid: Grid): Boolean {
    return position.row == 0 || position.col == 0
            || position.row == grid.size - 1 || position.col == grid.size - 1
}

private fun isLoop(position: Position): Boolean {
    return position.history.contains(Triple(position.direction, position.row, position.col))
}

private fun moveOrTurn(position: Position, grid: Grid) {
    position.history.add(Triple(position.direction, position.row, position.col))

    when (position.direction) {
        up ->
            if (grid[position.row - 1][position.col] == obstacle)
                position.direction = right
            else
                position.row--

        right ->
            if (grid[position.row][position.col + 1] == obstacle)
                position.direction = down
            else
                position.col++

        down ->
            if (grid[position.row + 1][position.col] == obstacle)
                position.direction = left
            else
                position.row++

        left ->
            if (grid[position.row][position.col - 1] == obstacle)
                position.direction = up
            else
                position.col--

        else -> throw IllegalStateException("Invalid direction, cannot move")
    }

    position.visited.add(Pair(position.row, position.col))
}

[–] [email protected] 2 points 2 weeks ago* (last edited 2 weeks ago)

Dart

Oof, simple rules can really trip you up if you don't pay attention.

~~This takes seconds to run so it's clearly not the right approach, but it get the right answers, so...~~

(edit) There's an interesting range of times from others as well, so it probably only requires a little more attention to get the time down, but maybe not today...

Click to reveal 60 lines of solution.

import 'dart:math';
import 'dart:math';
import 'package:more/more.dart';

var d4 = [Point(0, -1), Point(1, 0), Point(0, 1), Point(-1, 0)];

String safeGet(Point<int> p, List<List<String>> grid) =>
    (p.y.between(0, grid.length - 1) && p.x.between(0, grid.first.length - 1))
        ? grid[p.y][p.x]
        : '!';

Point<int> findGuard(List<List<String>> grid) {
  for (var row in grid.indices()) {
    for (var col in grid.first.indices()) {
      if (grid[row][col] == '^') return Point(col, row);
    }
  }
  return Point(-1, -1); //!
}

Set<Point<int>> getTrack(Point<int> guard, List<List<String>> grid) =>
    checkPath(guard, grid).first.map((e) => e.first).toSet();

// Check the path, returning ({(point, dir)}, did it end in a loop).
(Set<(Point<int>, int)>, bool) checkPath(
    Point<int> guard0, List<List<String>> grid,
    {Point<int>? block}) {
  if (block != null) grid[block.y][block.x] = '#';
  var dir = 0;
  var guard = guard0;
  var track = <(Point<int>, int)>{};
  var isLoop = false;
  while (safeGet(guard, grid) != '!') {
    if (track.contains((guard, dir))) {
      isLoop = true;
      break;
    }
    track.add((guard, dir));
    var check = guard + d4[dir];
    if (safeGet(check, grid) == '#') {
      dir = (dir + 1) % 4;
    } else {
      guard += d4[dir];
    }
  }
  if (block != null) grid[block.y][block.x] = '.';
  return (track, isLoop);
}

part1(List<String> lines) {
  var grid = lines.map((e) => e.split('')).toList();
  return getTrack(findGuard(grid), grid).length;
}

part2(List<String> lines) {
  var grid = lines.map((e) => e.split('')).toList();
  var guard = findGuard(grid);
  return getTrack(guard, grid)
      .count((e) => checkPath(guard, grid, block: e).last);
}

[–] [email protected] 2 points 2 weeks ago* (last edited 2 weeks ago)

python

Takes around 10s on my machine. I came up 10 short at first for part two and then realized sometimes you have to turn multiple times to avoid an obstacle.

solution

from dataclasses import dataclass
import aoc

@dataclass
class Guard():
    r: int = 0
    c: int = 0
    d: str = 'n'

    def move(self, lines):
        for _ in range(4):
            if self.d == 'n':
                if lines[self.r - 1][self.c] == '#':
                    self.d = 'e'
                else:
                    self.r -= 1
                    break
            elif self.d == 'e':
                if lines[self.r][self.c + 1] == '#':
                    self.d = 's'
                else:
                    self.c += 1
                    break
            elif self.d == 's':
                if lines[self.r + 1][self.c] == '#':
                    self.d = 'w'
                else:
                    self.r += 1
                    break
            elif self.d == 'w':
                if lines[self.r][self.c - 1] == '#':
                    self.d = 'n'
                else:
                    self.c -= 1
                    break

def setup():
    lines = aoc.get_lines(6, padded=(True, 'X', 1))
    g = Guard()
    for row, l in enumerate(lines):
        for col, c in enumerate(l):
            if c == '^':
                g.r, g.c = (row, col)
    s = (g.r, g.c)
    p = set()
    while lines[g.r][g.c] != 'X':
        p.add((g.r, g.c))
        g.move(lines)
    return (lines, g, s, p)

def one():
    print(len(setup()[3]))

def two():
    lines, g, s, p = setup()
    acc = 0
    for r, c in p:
        t = list(lines)
        ts = list(t[r])
        ts[c] = '#'
        t[r] = ''.join(ts)
        g.r, g.c, g.d = (s[0], s[1], 'n')
        v = set()
        while t[g.r][g.c] != 'X':
            st = (g.r, g.c, g.d)
            if st in v:
                acc += 1
                break
            v.add(st)
            g.move(t)
    print(acc)

one()
two()

[–] [email protected] 1 points 2 weeks ago* (last edited 2 weeks ago)

I'm not proud of it.

I have a conjecture though, that any looping solution, obtained by adding one obstacle, would eventually lead to a rectangular loop. That may lead to a non brute-force solution. It's quite hard to prove rigorously though. (Maybe proving, that the loop has to be convex, which is an equivalent statement here, is easier? You can also find matrix representations of the guard's state changes, if that helps.)

Maybe some of the more mathematically inclined people here can try proving or disproving that.

Anyways, here is my current solution in Kotlin:

fun main() {
    fun part1(input: List<String>): Int {
        val puzzleMap = PuzzleMap.fromPuzzleInput(input)
        puzzleMap.simulateGuardPath()
        return puzzleMap.asIterable().indicesWhere { it is MapObject.Visited }.count()
    }

    fun part2(input: List<String>): Int {
        val puzzleMap = PuzzleMap.fromPuzzleInput(input)
        puzzleMap.simulateGuardPath()

        return puzzleMap.asIterable().indicesWhere { it is MapObject.Visited }.count {
            val alteredPuzzleMap = PuzzleMap.fromPuzzleInput(input)
            alteredPuzzleMap[VecNReal(it)] = MapObject.Obstacle()
            alteredPuzzleMap.simulateGuardPath()
        }
    }

    val testInput = readInput("Day06_test")
    check(part1(testInput) == 41)
    check(part2(testInput) == 6)

    val input = readInput("Day06")
    part1(input).println()
    part2(input).println()
}

enum class Orientation {
    NORTH, SOUTH, WEST, EAST;

    fun rotateClockwise(): Orientation {
        return when (this) {
            NORTH -> EAST
            EAST -> SOUTH
            SOUTH -> WEST
            WEST -> NORTH
        }
    }
    
    fun asVector(): VecNReal {
        return when (this) {
            NORTH -> VecNReal(listOf(0.0, 1.0))
            SOUTH -> VecNReal(listOf(0.0, -1.0))
            WEST -> VecNReal(listOf(-1.0, 0.0))
            EAST -> VecNReal(listOf(1.0, 0.0))
        }
    }
}

class PuzzleMap(objectElements: List<List<MapObject>>): Grid2D<MapObject>(objectElements) {
    private val guard = Grid2D(objectElements).asIterable().first { it is MapObject.Guard } as MapObject.Guard

    companion object {
        fun fromPuzzleInput(input: List<String>): PuzzleMap = PuzzleMap(
            input.reversed().mapIndexed { y, row -> row.mapIndexed { x, cell ->  MapObject.fromCharAndIndex(cell, x to y) } }
        ).also { it.transpose() }
    }

    fun guardStep() {
        if (guardScout() is MapObject.Obstacle) guard.orientation = guard.orientation.rotateClockwise()
        else {
            guard.position += guard.orientation.asVector()
        }
    }

    fun simulateGuardPath(): Boolean {
        while (true) {
            markVisited()
            val scouted = guardScout()
            if (scouted is MapObject.Visited && guard.orientation in scouted.inOrientation) return true
            else if (scouted is MapObject.OutOfBounds) return false
            guardStep()
        }
    }

    fun guardScout(): MapObject = runCatching { this[guard.position + guard.orientation.asVector()] }.getOrElse { MapObject.OutOfBounds }

    fun markVisited() {
        val previousMapObject = this[guard.position]
        if (previousMapObject is MapObject.Visited) this[guard.position] = previousMapObject.copy(previousMapObject.inOrientation.plus(guard.orientation))
        else this[guard.position] = MapObject.Visited(listOf(guard.orientation))
    }
}

sealed class MapObject {
    class Empty: MapObject()
    class Obstacle: MapObject()
    object OutOfBounds: MapObject()

    data class Visited(val inOrientation: List<Orientation>): MapObject()
    data class Guard(var position: VecNReal, var orientation: Orientation = Orientation.NORTH): MapObject()

    companion object {
        fun fromCharAndIndex(c: Char, index: Pair<Int, Int>): MapObject {
            return when (c) {
                '.' -> Empty()
                '#' -> Obstacle()
                '^' -> Guard(VecNReal(index))
                else -> throw IllegalArgumentException("Unknown map object $c")
            }
        }
    }
}


I also have a repo.

[–] Quant 1 points 2 weeks ago

Uiua

Part one was simple enough. Part two nearly made me give up.
Part two has the most ugly and least performant code I've made in uiua so far but it gets the job done and that's all I care about for now.

Run with example input here

RotateClock ← (
  βŠ™βŠ™(β‰β‡Œ)
  βŠ™(β‡Œβœ(⊑0)(-βŠ™(⧻⊑0.)+1))
  ↻¯1
)

RotateCounter ← (
  βŠ™βŠ™(β‡Œβ‰)
  βŠ™(⍜(⊑0)(-βŠ™(⧻.)+1)β‡Œ)
  ↻1
)

NewPos ← (
  βŠ™βœ(βŠ™βŠ‘:)(-1+βŠ™(βŠ—@#)βŸœβ†˜βŠ™.)⟜°⊟
  ⍜(⊑1)β‹…
)

MarkPath ← (
  RotateClock
  ⍒( # replace characters up til next '#'
    βŠ™(βŠ™βœ(β†˜βŠ™βŠ‘:)(⍜(↙)(β–½:@^⧻)βŠ—@#.)⟜°⊟
      NewPos
    )
    RotateCounter
  | β‹…(β‰ 0⊑0))
  β—Œβ—Œ
)

PartOne ← (
  &rs ∞ &fo "input-6.txt"
  βŠœβˆ˜β‰ @\n.
  # maybe make compatible with
  # non-up facing inputs
  β™­βŠš=@^.
  [0 1 2 3]
  MarkPath
  &fwa "test.txt" json.
  /+/+=@^
)

PartTwo ← (
  &rs ∞ &fo "input-6.txt"
  βŠœβˆ˜β‰ @\n.
  # maybe make compatible with
  # non-up facing inputs
  β™­βŠš=@^.
  [0 1 2 3]
  β—‘MarkPath
  βŠ™::
  # rotate the field to match the intital state
  βŠ™βŠ™(
    βŠ™(⊚=@#)
    ⍒(β‡Œβ‰|Β¬β‰βŠš=@#)
    βŠ™β—Œ
  )
  βŠ™βŠ™(⊚=@^.)
  βŠ™βŠ™βŠ™Β€βˆ©Β€
  ⊞(βŠ™βŠ™(βœβŠ‘β‹…@#)
    RotateClock
    βŠ™NewPos
    €¯1_¯1_¯1
    ⍒(βŠ™β—‘(βŠ‚βŠ’)
      βŠ‚
      βŠ™(RotateCounter
        βŠ™NewPos
      )
    | =1+βŠ™(βˆˆβ†˜1β‡Œ)β—‘β‹…(β‰ 129⊑2)βŠ™(βŠ‚βŠ’))
    # 129 = length of input array. Hardcoded because
    # the condition block doesn't seem to get the
    # input array passed to it so the length can't
    # be read dynamically
    βŠ™(βŠ‚βŠ’)
    ∈
    βŠ™β—Œ
  )
  /+β™­
)

&p "Day 6:"
&pf "Part 1: "
&p PartOne
&pf "Part 2: "
&p PartTwo
[–] bugsmith 1 points 2 weeks ago

Gleam

Late as usual. This one challenged me. Functional programming is a lot of fun, but it's kicking my ass.

import gleam/dict
import gleam/io
import gleam/list
import gleam/option.{None, Some}
import gleam/result
import gleam/set.{type Set}
import gleam/string
import simplifile

pub type Point =
  #(Int, Int)

pub type Grid(a) =
  dict.Dict(Point, a)

pub type Direction {
  North
  East
  South
  West
}

pub type Loops {
  DoesLoop
  DoesNotLoop
}

pub type Guard {
  Guard(position: Point, direction: Direction)
}

fn get_guard(grid: Grid(String)) -> Guard {
  let pos = dict.filter(grid, fn(_pos, char) { char == "^" })
  let assert Ok(pos) = case dict.size(pos) {
    1 -> list.first(dict.keys(pos))
    0 -> panic as "No guard found in input!"
    _ -> panic as "More than one guard found in input!"
  }
  Guard(pos, North)
}

fn move_guard(guard: Guard) -> Guard {
  let new_pos = case guard.direction {
    North -> #(-1, 0)
    East -> #(0, 1)
    South -> #(1, 0)
    West -> #(0, -1)
  }
  Guard(
    #(guard.position.0 + new_pos.0, guard.position.1 + new_pos.1),
    guard.direction,
  )
}

fn turn_guard(guard: Guard) -> Guard {
  let new_dir = case guard.direction {
    North -> East
    East -> South
    South -> West
    West -> North
  }
  Guard(guard.position, new_dir)
}

fn get_obstacles(grid: Grid(String)) -> List(Point) {
  dict.filter(grid, fn(_pos, char) { char == "#" })
  |> dict.keys()
}

fn recurse_grid(
  grid: Grid(String),
  guard: Guard,
  obstacles: List(#(Int, Int)),
  visited: Set(#(#(Int, Int), Direction)),
) -> #(Set(#(#(Int, Int), Direction)), Loops) {
  let new_guard = move_guard(guard)
  let position = new_guard.position
  let dir = new_guard.direction
  case dict.has_key(grid, position) {
    False -> #(visited, DoesNotLoop)
    True -> {
      case set.contains(visited, #(position, dir)) {
        True -> {
          #(visited, DoesLoop)
        }
        False -> {
          case list.contains(obstacles, position) {
            True -> recurse_grid(grid, turn_guard(guard), obstacles, visited)
            False ->
              recurse_grid(
                grid,
                new_guard,
                obstacles,
                set.insert(visited, #(position, dir)),
              )
          }
        }
      }
    }
  }
}

fn get_grid_input(filename: String) -> Grid(String) {
  let lines =
    filename
    |> simplifile.read()
    |> result.unwrap("")
    |> string.trim()
    |> string.split("\n")
  use grid, row, row_idx <- list.index_fold(lines, dict.new())
  use grid, col, col_idx <- list.index_fold(string.to_graphemes(row), grid)
  dict.insert(grid, #(row_idx, col_idx), col)
}

fn part_one(
  grid: Grid(String),
) -> #(#(Set(#(#(Int, Int), Direction)), Loops), Int) {
  let guard = get_guard(grid)
  let obstacles = get_obstacles(grid)
  let visited = set.new() |> set.insert(#(guard.position, guard.direction))
  let visited = recurse_grid(grid, guard, obstacles, visited)
  let visited_without_dir =
    set.fold(visited.0, set.new(), fn(acc, x) { set.insert(acc, x.0) })
  #(visited, visited_without_dir |> set.size())
}

fn check_loop(grid: Grid(String), blocker: Point) -> Loops {
  let blocked_grid =
    dict.upsert(grid, blocker, fn(x) {
      case x {
        Some("^") -> "^"
        Some(_) -> "#"
        None -> "#"
      }
    })
  let visited = part_one(blocked_grid).0
  visited.1
}

fn part_two(grid: Grid(String), visited: Set(#(#(Int, Int), Direction))) {
  let visited =
    set.fold(visited, set.new(), fn(acc, x) { set.insert(acc, x.0) })
  use counter, position <- set.fold(visited, 0)
  case check_loop(grid, position) {
    DoesLoop -> counter + 1
    DoesNotLoop -> counter
  }
}

pub fn main() {
  let input = "input.in"
  let p1 = input |> get_grid_input() |> part_one
  let visited = p1.0.0
  io.debug(p1.1)
  input |> get_grid_input |> part_two(visited) |> io.debug()
}
[–] Andy 1 points 2 weeks ago* (last edited 2 weeks ago) (1 children)

Factor

spoiler

: get-input ( -- rows )
  "vocab:aoc-2024/06/input.txt" utf8 file-lines ;

: all-locations ( rows -- pairs )
  dimension <coordinate-matrix> concat ;

: guard-location ( rows -- pair )
  [ all-locations ] keep
  '[ _ matrix-nth "<>^v" in? ] find nip ;

TUPLE: state location char ;
C: <state> state

: guard-state ( rows -- state )
  [ guard-location ]
  [ dupd matrix-nth ] bi <state> ;

: faced-location ( state -- pair )
  [ char>> H{
    { CHAR: > { 0 1 } }
    { CHAR: v { 1 0 } }
    { CHAR: < { 0 -1 } }
    { CHAR: ^ { -1 0 } }
  } at ] [ location>> ] bi v+ ;

: off-grid? ( rows location -- ? )
  [ dimension ] dip
  [ v<= vany? ] keep
  { 0 0 } v< vany? or ;

: turn ( state -- state' )
  [ location>> ] [ char>> ] bi
  H{
    { CHAR: > CHAR: v }
    { CHAR: v CHAR: < }
    { CHAR: < CHAR: ^ }
    { CHAR: ^ CHAR: > }
  } at <state> ;

: obstacle? ( rows location -- ? )
  swap matrix-nth CHAR: # = ;

: guard-step ( rows state -- state' )
  swap over faced-location
  {
    { [ 2dup off-grid? ] [ 2nip f <state> ] }
    { [ [ obstacle? ] keep-under ] [ drop turn ] }
    [ swap char>> <state> ]
  } cond ;

: walk-out ( rows state -- trail )
  [
    [ 2dup location>> off-grid? ] [
      dup location>> ,
      dupd guard-step
    ] until
  ] { } make 2nip ;

: part1 ( -- n )
  get-input dup guard-state walk-out cardinality ;

: (walk-loops?) ( visited rows state -- looped? )
  dupd guard-step
  2dup location>> off-grid? [ 3drop f ] [
    pick dupd in? [ 3drop t ] [
      pick dupd adjoin (walk-loops?)
    ] if
  ] if ;

: walk-loops? ( rows -- looped? )
  dup guard-state
  [ HS{ } clone ] 2dip
  pick dupd adjoin (walk-loops?) ;

: obstacle-candidates ( rows -- pairs )
  [ guard-location ]
  [ dup guard-state walk-out members ] bi remove ;

: part2 ( -- n )
  get-input dup obstacle-candidates
  [ CHAR: # spin deep-clone [ matrix-set-nth ] keep walk-loops? ] with count ;

[–] Andy 1 points 2 weeks ago* (last edited 2 weeks ago)

Nothing smart to see here. I may revisit this when I give up on future days.