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