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