Rust
Blunt force grid navigation https://gitlab.com/landreville/advent-of-code-2024/-/blob/main/src/bin/04.rs
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.
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 | 18 | 20 | 21 | 22 |
23 | 24 | 25 |
Icon base by Lorc under CC BY 3.0 with modifications to add a gradient
console.log('Hello World')
Blunt force grid navigation https://gitlab.com/landreville/advent-of-code-2024/-/blob/main/src/bin/04.rs
Ugh. Spent way too long on today's. Should have just used my own grid structure from last year. I will likely refactor to use that. Even though it's likely a super slow implementation, the convenience of dealing with it is better than shoehorning in the grid::Grid<T>
from that crate.
solution (no supporting code)
use grid::Grid;
use crate::shared::{
grid2d::{iter_diag_nesw, iter_diag_nwse, Point},
util::read_lines,
};
fn parse_grid(input: &[String]) -> Grid<u8> {
let cols = input.first().unwrap().len();
Grid::from_vec(
input
.iter()
.flat_map(|row| row.chars().map(|c| c as u8).collect::<Vec<u8>>())
.collect(),
cols,
)
}
fn part1(grid: &Grid<u8>) -> usize {
let mut xmas_count = 0;
let rows = grid
.iter_rows()
.map(|d| String::from_utf8(d.copied().collect()).unwrap());
let cols = grid
.iter_cols()
.map(|d| String::from_utf8(d.copied().collect()).unwrap());
for diag in iter_diag_nesw(grid)
.chain(iter_diag_nwse(grid))
.filter_map(|d| {
if d.len() >= 4 {
Some(String::from_utf8(d.clone()).unwrap())
} else {
None
}
})
.chain(rows)
.chain(cols)
{
xmas_count += diag.matches("XMAS").count() + diag.matches("SAMX").count()
}
xmas_count
}
fn part2(grid: &Grid<u8>) -> usize {
let mut xmas_count = 0;
let valid = [
[b'M', b'M', b'S', b'S'],
[b'M', b'S', b'S', b'M'],
[b'S', b'M', b'M', b'S'],
[b'S', b'S', b'M', b'M'],
];
for x in 1..grid.cols() - 1 {
for y in 1..grid.rows() - 1 {
if grid.get(y, x) == Some(&b'A')
&& valid.contains(
&(Point::new(x as isize, y as isize)
.diagonal_neighbors(grid)
.map(|i| i.unwrap_or(0))),
)
{
xmas_count += 1;
}
}
}
xmas_count
}
pub fn solve() {
let input = read_lines("inputs/day04.txt");
let grid = parse_grid(&input);
println!("Part 1: {}", part1(&grid));
println!("Part 2: {}", part2(&grid));
}
And here's a link to the Github if you care to see the gross supporting code :D
import ../aoc, strutils
type
Cell* = tuple[x,y:int]
#the 8 grid direction
const directions : array[8, Cell] = [
(1, 0), (-1, 0),
(0, 1), ( 0,-1),
(1, 1), (-1,-1),
(1,-1), (-1, 1)
]
const xmas = "XMAS"
#part 1
proc searchXMAS*(grid:seq[string], x,y:int):int =
#search in all 8 directions (provided we can find a full match in that direction)
let w = grid[0].len
let h = grid.len
for dir in directions:
# check if XMAS can even fit
let xEnd = x + dir.x * 3
let yEnd = y + dir.y * 3
if xEnd < 0 or xEnd >= w or
yEnd < 0 or yEnd >= h:
continue;
#step along direction
var matches = 0
for s in 0..3:
if grid[y + dir.y * s][x + dir.x * s] == xmas[s]:
inc matches
if matches == xmas.len:
inc result
#part 2
proc isMAS(grid:seq[string], c, o:Cell):bool=
let ca : Cell = (c.x+o.x, c.y+o.y)
let cb : Cell = (c.x-o.x, c.y-o.y)
let a = grid[ca.y][ca.x]
let b = grid[cb.y][cb.x]
(a == 'M' and b == 'S') or (a == 'S' and b == 'M')
proc searchCrossMAS*(grid:seq[string], x,y:int):bool =
grid[y][x] == 'A' and
grid.isMAS((x,y), (1,1)) and
grid.isMAS((x,y), (1,-1))
proc solve*(input:string): array[2,int] =
let grid = input.splitLines
let w = grid[0].len
let h = grid.len
#part 1
for y in 0..<h:
for x in 0..<w:
result[0] += grid.searchXMAS(x, y)
#part 2, skipping borders
for y in 1..<h-1:
for x in 1..<w-1:
result[1] += (int)grid.searchCrossMAS(x, y)
Part 1 was done really quickly. Part 2 as well, but the result was not accepted...
Turns out +MAS isn't actually a thing :P
Had some time to clean up the code today since the solution was quite straight forward after making a plan on how to approach it.
spoiler
function readWordSearch(inputFile::String)::Matrix{Char}
f = open(inputFile,"r")
lines::Vector{String} = readlines(f)
close(f)
wordSearch = Matrix{Char}(undef,length(lines),length(lines))
for (i,line) in enumerate(lines)
wordSearch[i,:] = collect(line)
end
return wordSearch
end
function countXMASAppearances(wS::Matrix{Char})::Int
appearanceCount::Int = 0
for i=1 : size(wS)[1] #lines
for j=1 : size(wS)[2] #columns
wS[i,j]!='X' ? continue : nothing #continue if char is not X
#if char is X, check surrounding area
# check horizontals
#left
j>=4 ? (wS[i,j-1]*wS[i,j-2]*wS[i,j-3]=="MAS" ? appearanceCount+=1 : nothing) : nothing
#right
j<=size(wS)[2]-3 ? (wS[i,j+1]*wS[i,j+2]*wS[i,j+3]=="MAS" ? appearanceCount+=1 : nothing) : nothing
# check verticals
#up
i>=4 ? (wS[i-1,j]*wS[i-2,j]*wS[i-3,j]=="MAS" ? appearanceCount+=1 : nothing) : nothing
#down
i<=size(wS)[1]-3 ? (wS[i+1,j]*wS[i+2,j]*wS[i+3,j]=="MAS" ? appearanceCount+=1 : nothing) : nothing
# check diagonals
#left up
i>=4 && j>=4 ? (wS[i-1,j-1]*wS[i-2,j-2]*wS[i-3,j-3]=="MAS" ? appearanceCount+=1 : nothing) : nothing
#right up
i>=4 && j<=size(wS)[2]-3 ? (wS[i-1,j+1]*wS[i-2,j+2]*wS[i-3,j+3]=="MAS" ? appearanceCount+=1 : nothing) : nothing
#left down
i<=size(wS)[1]-3 && j>=4 ? (wS[i+1,j-1]*wS[i+2,j-2]*wS[i+3,j-3]=="MAS" ? appearanceCount+=1 : nothing) : nothing
#right down
i<=size(wS)[1]-3 && j<=size(wS)[2]-3 ? (wS[i+1,j+1]*wS[i+2,j+2]*wS[i+3,j+3]=="MAS" ? appearanceCount+=1 : nothing) : nothing
end
end
return appearanceCount
end
function countX_MASAppearances(wordSearch::Matrix{Char})::Int
appearances::Int = 0
for l=2 : size(wordSearch)[1]-1
for c=2 : size(wordSearch)[2]-1
wordSearch[l,c]!='A' ? continue : nothing
checkArr = [wordSearch[l-1,c-1],wordSearch[l-1,c+1],wordSearch[l+1,c-1],wordSearch[l+1,c+1]]
if checkArr in [['M','M','S','S'],['M','S','M','S'],['S','S','M','M'],['S','M','S','M']]
appearances += 1
end
end
end
return appearances
end
wordSearch::Matrix{Char} = readWordSearch(inputFile
prinltn("part 1 appearances: $(countXMASAppearances(wordSearch))")
prinltn("part 2 appearances: $(countX_MASAppearances(wordSearch))")
def read_input(path):
with open(path) as f:
lines = f.readlines()
for i, line in enumerate(lines):
ln = line.replace("\n","")
lines[i] = ln
return lines
def find_X(lines):
Xes = []
for j, line in enumerate(lines):
ind = [i for i, ltr in enumerate(line) if ltr == "X"]
for i in ind:
Xes.append((j,i))
return Xes
def find_M(lines, x, dim):
# Check for Ms
M_dirs = []
for i in [-1, 0, 1]:
x_ind = x[0] + i
if x_ind>=0 and x_ind<dim:
for j in [-1, 0, 1]:
y_ind = x[1]+j
if y_ind>=0 and y_ind<dim:
if lines[x_ind][y_ind] == "M":
M = [(x_ind, y_ind), (i,j)]
M_dirs.append(M)
return M_dirs
def check_surroundings(loc, lines, check_char, direction):
max = len(lines)-1
check_lock = [loc[i]+direction[i] for i in range(len(loc))]
if all(i>=0 and i<=max for i in check_lock) and check_char in str(lines[check_lock[0]][check_lock[1]]):
return True
else:
return False
def part_one(lines):
ans = 0
X = find_X(lines)
dim = len(lines[0])
for x in X:
M = find_M(lines, x, dim)
for m in M:
loc = m[0]
dir = m[1]
if not check_surroundings(loc, lines, 'A', dir):
continue
loc = [loc[0]+dir[0], loc[1]+dir[1]]
if not all(i>=0 and i<=dim-1 for i in loc):
continue
if not check_surroundings(loc, lines, 'S', dir):
continue
ans+=1
return ans
def extract_square(lines, loc):
str = ""
for i in range(-1,2,1):
for j in range(-1,2,1):
x_ind = loc[0]+i
y_ind = loc[1]+j
if not all(p>=0 and p<=len(lines[0])-1 for p in [x_ind, y_ind]):
raise ValueError("The given lock is at the edge of the grid and therefore will not produce a square")
str += lines[x_ind][y_ind]
return str
def check_square(square):
if not square[4]=="A":
return False
elif not ((square[0]=="M" and square[8]=="S") or (square[0]=="S" and square[8]=="M")):
return False
elif not ((square[2]=="M" and square[6]=="S") or (square[2]=="S" and square[6]=="M")):
return False
else: return True
def part_two(lines):
ans = 0
dim = len(lines[0])
for i in range(1,dim-1):
for j in range(1,dim-1):
square = extract_square(lines, (i,j))
if check_square(square):
ans += 1
return ans
path = r'Day_4\input.txt'
lines = read_input(path)
print("Answer part 1: ", part_one(lines))
print("Answer part 2: ", part_two(lines))
spoiler
: get-input ( -- rows )
"vocab:aoc-2024/04/input.txt" utf8 file-lines ;
: verticals ( rows -- lines )
[ dimension last [0..b) ] keep cols ;
: slash-origins ( dimension -- coords )
[ first [0..b) [ 0 2array ] map ] [
first2 [ 1 - ] [ 1 (a..b] ] bi*
[ 2array ] with map
] bi append ;
: backslash-origins ( dimension -- coords )
first2
[ [0..b) [ 0 2array ] map ]
[ 1 (a..b] [ 0 swap 2array ] map ] bi* append ;
: slash ( rows origin -- line )
first2
[ 0 [a..b] ]
[ pick dimension last [a..b) ] bi* zip
swap matrix-nths ;
: backslash ( rows origin -- line )
[ dup dimension ] dip first2
[ over first [a..b) ]
[ pick last [a..b) ] bi* zip nip
swap matrix-nths ;
: slashes ( rows -- lines )
dup dimension slash-origins
[ slash ] with map ;
: backslashes ( rows -- lines )
dup dimension backslash-origins
[ backslash ] with map ;
: word-count ( line word -- n )
dupd [ reverse ] dip
'[ _ subseq-indices length ] bi@ + ;
: part1 ( -- n )
get-input
{ [ ] [ verticals ] [ slashes ] [ backslashes ] } cleave-array concat
[ "XMAS" word-count ] map-sum ;
: origin-adistances ( rows origins line-quot: ( rows origin -- line ) -- origin-adistances-assoc )
with zip-with
"MAS" "SAM" [ '[ [ _ subseq-indices ] map-values ] ] bi@ bi append
harvest-values
[ [ 1 + ] map ] map-values ; inline
: a-coords ( origin-adistances coord-quot: ( adistance -- row-delta col-delta ) -- coords )
'[ first2 [ @ 2array v+ ] with map ] map-concat ; inline
: slash-a-coords ( rows -- coords )
dup dimension slash-origins [ slash ] origin-adistances
[ [ 0 swap - ] keep ] a-coords ;
: backslash-a-coords ( rows -- coords )
dup dimension backslash-origins [ backslash ] origin-adistances
[ dup ] a-coords ;
: part2 ( -- n )
get-input [ slash-a-coords ] [ backslash-a-coords ] bi
intersect length ;
Better viewed on GitHub.
This one was a little bit of a pain. I loved it.
Solution
import { AdventOfCodeSolutionFunction } from "./solutions";
enum Direction {
UP,
UP_RIGHT,
RIGHT,
BOTTOM_RIGHT,
BOTTOM,
BOTTOM_LEFT,
LEFT,
UP_LEFT,
};
const ALL_DIRECTIONS = [
Direction.RIGHT,
Direction.BOTTOM_RIGHT,
Direction.BOTTOM,
Direction.BOTTOM_LEFT,
Direction.LEFT,
Direction.UP_LEFT,
Direction.UP,
Direction.UP_RIGHT,
];
const check_coords = (grid: Array<Array<string>>, x: number, y: number) => {
return y >= grid.length ||
y < 0 ||
x >= grid[y].length ||
x < 0
}
const search_direction = (grid: Array<Array<string>>, x: number, y: number, direction: Direction, find: Array<string>) => {
// exit conditions
// no more to find
if (find.length == 0)
return 1; // found the end
// invalid coords
if (check_coords(grid, x, y))
return 0;
// make new mutable list
const newFind = [...find];
const searchChar = newFind.shift();
// wrong character
if (grid[y][x] !== searchChar)
return 0;
switch (direction) {
case Direction.UP:
return search_direction(grid, x, y + 1, direction, newFind);
case Direction.UP_RIGHT:
return search_direction(grid, x + 1, y + 1, direction, newFind);
case Direction.RIGHT:
return search_direction(grid, x + 1, y, direction, newFind);
case Direction.BOTTOM_RIGHT:
return search_direction(grid, x + 1, y - 1, direction, newFind);
case Direction.BOTTOM:
return search_direction(grid, x, y - 1, direction, newFind);
case Direction.BOTTOM_LEFT:
return search_direction(grid, x - 1, y - 1, direction, newFind);
case Direction.LEFT:
return search_direction(grid, x - 1, y, direction, newFind);
case Direction.UP_LEFT:
return search_direction(grid, x - 1, y + 1, direction, newFind);
default:
return 0;
}
}
const part_1_search = (grid: Array<Array<string>>, x: number, y: number, find: Array<string>) => {
return ALL_DIRECTIONS.reduce<number>(
(instances, direction) =>
instances + search_direction(grid, x, y, direction, find),
0
);
}
const part_2_search = (grid: Array<Array<string>>, x: number, y: number, find: Array<string>) => {
return (
search_direction(grid, x - 1, y + 1, Direction.BOTTOM_RIGHT, find) +
search_direction(grid, x + 1, y + 1, Direction.BOTTOM_LEFT, find) +
search_direction(grid, x - 1, y - 1, Direction.UP_RIGHT, find) +
search_direction(grid, x + 1, y - 1, Direction.UP_LEFT, find)
) == 2 ? 1 : 0;
}
export const solution_4: AdventOfCodeSolutionFunction = (input) => {
const grid = input.split("\n").map(st => st.trim()).map(v => v.split(""));
let part_1 = 0;
let part_2 = 0;
const find_1 = "XMAS".split("");
const find_2 = "MAS".split("");
for (let y = 0; y < grid.length; y++) {
for (let x = 0; x < grid[y].length; x++) {
part_1 += part_1_search(grid, x, y, find_1);
part_2 += part_2_search(grid, x, y, find_2);
}
}
return {
part_1,
part_2,
};
}
Felt like this code quality is better than what I usually output :)
Just a bunch of ifs and bounds checking. Part 2 was actually simpler.
Code
func part1(W [][]rune) {
m := len(W)
n := len(W[0])
xmasCount := 0
for i := 0; i < m; i++ {
for j := 0; j < n; j++ {
if W[i][j] != 'X' {
continue
}
if j < n-3 && W[i][j+1] == 'M' && W[i][j+2] == 'A' && W[i][j+3] == 'S' {
// Horizontal left to right
xmasCount++
}
if j >= 3 && W[i][j-1] == 'M' && W[i][j-2] == 'A' && W[i][j-3] == 'S' {
// Horizontal right to left
xmasCount++
}
if i < m-3 && W[i+1][j] == 'M' && W[i+2][j] == 'A' && W[i+3][j] == 'S' {
// Vertical up to down
xmasCount++
}
if i >= 3 && W[i-1][j] == 'M' && W[i-2][j] == 'A' && W[i-3][j] == 'S' {
// Vertical down to up
xmasCount++
}
if j < n-3 && i < m-3 && W[i+1][j+1] == 'M' && W[i+2][j+2] == 'A' && W[i+3][j+3] == 'S' {
// Diagonal left to right and up to down
xmasCount++
}
if j >= 3 && i < m-3 && W[i+1][j-1] == 'M' && W[i+2][j-2] == 'A' && W[i+3][j-3] == 'S' {
// Diagonal right to left and up to down
xmasCount++
}
if j < n-3 && i >= 3 && W[i-1][j+1] == 'M' && W[i-2][j+2] == 'A' && W[i-3][j+3] == 'S' {
// Diagonal left to right and down to up
xmasCount++
}
if j >= 3 && i >= 3 && W[i-1][j-1] == 'M' && W[i-2][j-2] == 'A' && W[i-3][j-3] == 'S' {
// Diagonal right to left and down to up
xmasCount++
}
}
}
fmt.Println(xmasCount)
}
func part2(W [][]rune) {
m := len(W)
n := len(W[0])
xmasCount := 0
for i := 0; i <= m-3; i++ {
for j := 0; j <= n-3; j++ {
if W[i+1][j+1] != 'A' {
continue
}
if W[i][j] == 'M' && W[i][j+2] == 'M' && W[i+2][j] == 'S' && W[i+2][j+2] == 'S' {
xmasCount++
} else if W[i][j] == 'M' && W[i][j+2] == 'S' && W[i+2][j] == 'M' && W[i+2][j+2] == 'S' {
xmasCount++
} else if W[i][j] == 'S' && W[i][j+2] == 'S' && W[i+2][j] == 'M' && W[i+2][j+2] == 'M' {
xmasCount++
} else if W[i][j] == 'S' && W[i][j+2] == 'M' && W[i+2][j] == 'S' && W[i+2][j+2] == 'M' {
xmasCount++
}
}
}
fmt.Println(xmasCount)
}
func main() {
file, _ := os.Open("input.txt")
defer file.Close()
scanner := bufio.NewScanner(file)
var W [][]rune
for scanner.Scan() {
line := scanner.Text()
W = append(W, []rune(line))
}
part1(W)
part2(W)
}
Oof, my struggle to make custom index walking paths for part 1 did not pay off for part 2.
Solution
sub MAIN($input) {
my $file = (open $input).slurp;
my @grid is List = $file.lines».comb».list;
my @transposedGrid is List = [Z] @grid;
my @reversedGrid is List = @grid».reverse;
my @transposedReversedGrid is List = @transposedGrid».reverse;
my @horizontalScanRows is List = generateScanHorizontal(@grid);
my @transposedHorizontalScanRows is List = generateScanHorizontal(@transposedGrid);
my @part-one-counts = [];
@part-one-counts.push(count-xmas(@grid, @horizontalScanRows)); # Right
@part-one-counts.push(count-xmas(@transposedGrid, @transposedHorizontalScanRows)); # Down
@part-one-counts.push(count-xmas(@reversedGrid, @horizontalScanRows)); # Left
@part-one-counts.push(count-xmas(@transposedReversedGrid, @transposedHorizontalScanRows)); # Up
my @diagonalScanRows is List = generateScanDiagonal(@grid);
my @transposedDiagonalScanRows is List = generateScanDiagonal(@transposedGrid);
@part-one-counts.push(count-xmas(@grid, @diagonalScanRows)); # Down Right
@part-one-counts.push(count-xmas(@grid, @diagonalScanRows».reverse)); # Up Left
@part-one-counts.push(count-xmas(@reversedGrid, @diagonalScanRows)); # Down Left
@part-one-counts.push(count-xmas(@reversedGrid, @diagonalScanRows».reverse)); # Up Right
my $part-one-solution = @part-one-counts.sum;
say "part 1: $part-one-solution";
my @part-two-counts = [];
@part-two-counts.push(countGridMatches(@grid, (<M . S>,<. A .>,<M . S>)));
@part-two-counts.push(countGridMatches(@grid, (<S . S>,<. A .>,<M . M>)));
@part-two-counts.push(countGridMatches(@grid, (<S . M>,<. A .>,<S . M>)));
@part-two-counts.push(countGridMatches(@grid, (<M . M>,<. A .>,<S . S>)));
my $part-two-solution = @part-two-counts.sum;
say "part 2: $part-two-solution";
}
sub count-xmas(@grid, @scanRows) {
my $xmas-count = 0;
for @scanRows -> @scanRow {
my $xmas-pos = 0;
for @scanRow -> @pos {
my $char = @grid[@pos[0]][@pos[1]];
if "X" eq $char {
$xmas-pos = 1;
}elsif <X M A S>[$xmas-pos] eq $char {
if $xmas-pos == 3 {
$xmas-pos = 0;
$xmas-count += 1;
} else {
$xmas-pos += 1;
}
} else {
$xmas-pos = 0;
}
}
}
return $xmas-count;
}
sub generateScanHorizontal(@grid) {
# Horizontal
my $rows = @grid.elems;
my $cols = @grid[0].elems;
my @scanRows = ();
for 0..^$rows -> $row {
my @scanRow = ();
for 0..^$cols -> $col {
@scanRow.push(($row, $col));
}
@scanRows.push(@scanRow);
}
return @scanRows.List».List;
}
sub generateScanDiagonal(@grid) {
# Down-right diagonal
my $rows = @grid.elems;
my $cols = @grid[0].elems;
my @scanRows = ();
for 0..^($rows + $cols - 1) -> $diag {
my @scanRow = ();
my $starting-row = max(-$cols + $diag + 1, 0);
my $starting-col = max($rows - $diag - 1, 0);
my $diag-len = min($rows - $starting-row, $cols - $starting-col);
for 0..^$diag-len -> $diag-pos {
@scanRow.push(($starting-row + $diag-pos, $starting-col + $diag-pos));
}
@scanRows.push(@scanRow);
}
return @scanRows.List».List;
}
sub countGridMatches(@grid, @needle) {
my $count = 0;
for 0..(@grid.elems - @needle.elems) -> $top {
TOP-LEFT:
for 0..(@grid[$top].elems - @needle[0].elems) -> $left {
for 0..^@needle.elems -> $row-offset {
for 0..^@needle[$row-offset].elems -> $col-offset {
my $needle-char = @needle[$row-offset][$col-offset];
next if $needle-char eq ".";
next TOP-LEFT if $needle-char ne @grid[$top+$row-offset][$left+$col-offset];
}
}
$count += 1;
}
}
return $count;
}
namespace Day04;
static class Program
{
public record struct Point(int Row, int Col);
static void Main(string[] args)
{
var sample = File.ReadAllLines("sample.txt");
var data = File.ReadAllLines("data.txt");
Console.WriteLine($"Part 1 (sample): {SolvePart1(sample)}");
Console.WriteLine($"Part 1 (data): {SolvePart1(data)}");
Console.WriteLine($"Part 2 (sample): {SolvePart2(sample)}");
Console.WriteLine($"Part 2 (data): {SolvePart2(data)}");
}
private static readonly string Search = "XMAS";
private static readonly Func<Point, Point>[] DirectionalMoves =
{
p => new Point(p.Row + 1, p.Col),
p => new Point(p.Row + 1, p.Col + 1),
p => new Point(p.Row, p.Col + 1),
p => new Point(p.Row - 1, p.Col + 1),
p => new Point(p.Row - 1, p.Col),
p => new Point(p.Row - 1, p.Col - 1),
p => new Point(p.Row, p.Col - 1),
p => new Point(p.Row + 1, p.Col - 1),
};
private static readonly Func<Point, Point>[] ForwardSlashMoves =
{
p => new Point(p.Row - 1, p.Col - 1),
p => new Point(p.Row + 1, p.Col + 1),
};
private static readonly Func<Point, Point>[] BackSlashMoves =
{
p => new Point(p.Row + 1, p.Col - 1),
p => new Point(p.Row - 1, p.Col + 1),
};
static long SolvePart1(string[] data)
{
return Enumerable
.Range(0, data.Length)
.SelectMany(row => Enumerable.Range(0, data[row].Length)
.Select(col => new Point(row, col)))
.Where(p => IsMatch(data, p, Search[0]))
.Sum(p => DirectionalMoves
.Count(move => DeepMatch(data, move(p), move, Search, 1)));
}
static long SolvePart2(string[] data)
{
return Enumerable
.Range(0, data.Length)
.SelectMany(row => Enumerable.Range(0, data[row].Length)
.Select(col => new Point(row, col)))
.Where(p => IsMatch(data, p, 'A'))
.Count(p => CheckDiagonalMoves(data, p, ForwardSlashMoves)
&& CheckDiagonalMoves(data, p, BackSlashMoves));
}
static bool CheckDiagonalMoves(string[] data, Point p, Func<Point, Point>[] moves)
=> (IsMatch(data, moves[0](p), 'S') && IsMatch(data, moves[1](p), 'M'))
|| (IsMatch(data, moves[0](p), 'M') && IsMatch(data, moves[1](p), 'S'));
static bool DeepMatch(string[] data, Point p, Func<Point, Point> move, string search, int searchIndex) =>
(searchIndex >= search.Length) ? true :
(!IsMatch(data, p, search[searchIndex])) ? false :
DeepMatch(data, move(p), move, search, searchIndex + 1);
static bool IsMatch(string[] data, Point p, char searchChar)
=> IsInBounds(data, p) && (data[p.Row][p.Col] == searchChar);
static bool IsInBounds(string[] data, Point p) =>
(p.Row >= 0) && (p.Col >= 0) && (p.Row < data.Length) && (p.Col < data[0].Length);
}
fun part1(input: String): Int {
return countWordOccurrences(input.lines())
}
fun part2(input: String): Int {
val grid = input.lines().map(String::toList)
var count = 0
for (row in 1..grid.size - 2) {
for (col in 1..grid[row].size - 2) {
if (grid[row][col] == 'A') {
count += countCrossMatch(grid, row, col)
}
}
}
return count
}
private fun countCrossMatch(grid: List<List<Char>>, row: Int, col: Int): Int {
val surroundingCorners = listOf(
grid[row - 1][col - 1], // upper left
grid[row - 1][col + 1], // upper right
grid[row + 1][col - 1], // lower left
grid[row + 1][col + 1], // lower right
)
// no matches:
// M S S M
// A A
// S M M S
return if (surroundingCorners.count { it == 'M' } == 2
&& surroundingCorners.count { it == 'S' } == 2
&& surroundingCorners[0] != surroundingCorners[3]
) 1 else 0
}
private fun countWordOccurrences(matrix: List<String>): Int {
val rows = matrix.size
val cols = if (rows > 0) matrix[0].length else 0
val directions = listOf(
Pair(0, 1), // Horizontal right
Pair(1, 0), // Vertical down
Pair(1, 1), // Diagonal down-right
Pair(1, -1), // Diagonal down-left
Pair(0, -1), // Horizontal left
Pair(-1, 0), // Vertical up
Pair(-1, -1), // Diagonal up-left
Pair(-1, 1) // Diagonal up-right
)
fun isWordAt(row: Int, col: Int, word: String, direction: Pair<Int, Int>): Boolean {
val (dx, dy) = direction
for (i in word.indices) {
val x = row + i * dx
val y = col + i * dy
if (x !in 0 until rows || y !in 0 until cols || matrix[x][y] != word[i]) {
return false
}
}
return true
}
var count = 0
for (row in 0 until rows) {
for (col in 0 until cols) {
for (direction in directions) {
if (isWordAt(row, col, "XMAS", direction)) {
count++
}
}
}
}
return count
}
Just part1 for now as I need to walk the dog :-)
[edit] Part 2 now added, and a nicer approach than Part 1 in my opinion, if you're able to keep that many dimensions straight in your head :-)
[edit 2] Tightened it up a bit more.
Grid ← ⊜∘⊸≠@\n "MMMSXXMASM\nMSAMXMSMSA\nAMXSXMAAMM\nMSAMASMSMX\nXMASAMXAMM\nXXAMMXXAMA\nSMSMSASXSS\nSAXAMASAAA\nMAMMMXMMMM\nMXMXAXMASX"
≡⍉⍉×⇡4¤[1_0 0_1 1_1 1_¯1] # Use core dirs to build sets of 4-offsets.
↯∞_2⇡△ Grid # Get all possible starting points.
&p/+♭⊞(+∩(≍"XMAS")⇌.⬚@.⊡:Grid≡+¤) # Part 1. Join the two into a table, use to pick 4-elements, check, count.
Diags ← [[¯. 1_1] [¯. 1_¯1]]
BothMas ← /×≡(+∩(≍"MS")⇌.)⬚@.⊡≡+Diags¤¤ # True if both diags here are MAS.
&p/+≡BothMas⊚="A"⟜¤Grid # Part 2. For all "A"s in grid, check diags, count where good.
I'm not even sure how to write most of these characters
The operators have all got ascii names you can type, and the formatter converts them to the symbols. It's a bit odd but really worthwhile, as you get access to the powerful array handling functionality that made solving today's challenges so much more straightforward than in other languages.
It looks quite functional indeed
What can I say, bunch of for loops! I add a 3 cell border to avoid having to do bounds checking in the inner loops.
Code
#include "common.h"
#define GZ 146
int main(int argc, char **argv) {
static char g[GZ][GZ];
static const char w[] = "XMAS";
int p1=0,p2=0, x,y, m,i;
if (argc > 1) DISCARD(freopen(argv[1], "r", stdin));
for (y=3; y<GZ && fgets(g[y]+3, GZ-3, stdin); y++) ;
for (y=3; y<GZ-3; y++)
for (x=3; x<GZ-3; x++) {
for (m=1,i=0; i<4; i++) {m &= g[y+i][x]==w[i];} p1+=m;
for (m=1,i=0; i<4; i++) {m &= g[y-i][x]==w[i];} p1+=m;
for (m=1,i=0; i<4; i++) {m &= g[y][x+i]==w[i];} p1+=m;
for (m=1,i=0; i<4; i++) {m &= g[y][x-i]==w[i];} p1+=m;
for (m=1,i=0; i<4; i++) {m &= g[y+i][x+i]==w[i];} p1+=m;
for (m=1,i=0; i<4; i++) {m &= g[y-i][x-i]==w[i];} p1+=m;
for (m=1,i=0; i<4; i++) {m &= g[y+i][x-i]==w[i];} p1+=m;
for (m=1,i=0; i<4; i++) {m &= g[y-i][x+i]==w[i];} p1+=m;
p2 += g[y+1][x+1]=='A' &&
((g[y][x] =='M' && g[y+2][x+2]=='S') ||
(g[y][x] =='S' && g[y+2][x+2]=='M')) &&
((g[y+2][x]=='M' && g[y][x+2] =='S') ||
(g[y+2][x]=='S' && g[y][x+2] =='M'));
}
printf("04: %d %d\n", p1, p2);
}
Could be done more elegantly, but I haven’t bothered yet.
proc solve(input: string): AOCSolution[int, int] =
var lines = input.splitLines()
block p1:
# horiz
for line in lines:
for i in 0..line.high-3:
if line[i..i+3] in ["XMAS", "SAMX"]:
inc result.part1
for y in 0..lines.high-3:
#vert
for x in 0..lines[0].high:
let word = collect(for y in y..y+3: lines[y][x])
if word in [@"XMAS", @"SAMX"]:
inc result.part1
#diag \
for x in 0..lines[0].high-3:
let word = collect(for d in 0..3: lines[y+d][x+d])
if word in [@"XMAS", @"SAMX"]:
inc result.part1
#diag /
for x in 3..lines[0].high:
let word = collect(for d in 0..3: lines[y+d][x-d])
if word in [@"XMAS", @"SAMX"]:
inc result.part1
block p2:
for y in 0..lines.high-2:
for x in 0..lines[0].high-2:
let diagNW = collect(for d in 0..2: lines[y+d][x+d])
let diagNE = collect(for d in 0..2: lines[y+d][x+2-d])
if diagNW in [@"MAS", @"SAM"] and diagNE in [@"MAS", @"SAM"]:
inc result.part2
I struggled a lot more when doing list slices that I would've liked to
import Data.List qualified as List
collectDiagonal :: [String] -> Int -> Int -> String
collectDiagonal c y x
| length c > y && length (c !! y) > x = c !! y !! x : collectDiagonal c (y+1) (x+1)
| otherwise = []
part1 c = do
let forwardXMAS = map (length . filter (List.isPrefixOf "XMAS") . List.tails) $ c
let backwardXMAS = map (length . filter (List.isPrefixOf "XMAS") . List.tails . reverse) $ c
let downwardXMAS = map (length . filter (List.isPrefixOf "XMAS") . List.tails ) . List.transpose $ c
let upwardXMAS = map (length . filter (List.isPrefixOf "XMAS") . List.tails . reverse ) . List.transpose $ c
let leftSideDiagonals = map (\ y -> collectDiagonal c y 0) [0..length c]
let leftTopDiagonals = map (\ x -> collectDiagonal c 0 x) [1..(length . List.head $ c)]
let leftDiagonals = leftSideDiagonals ++ leftTopDiagonals
let rightSideDiagonals = map (\ y -> collectDiagonal (map List.reverse c) y 0) [0..length c]
let rightTopDiagonals = map (\ x -> collectDiagonal (map List.reverse c) 0 x) [1..(length . List.head $ c)]
let rightDiagonals = rightSideDiagonals ++ rightTopDiagonals
let diagonals = leftDiagonals ++ rightDiagonals
let diagonalXMAS = map (length . filter (List.isPrefixOf "XMAS") . List.tails) $ diagonals
let reverseDiagonalXMAS = map (length . filter (List.isPrefixOf "XMAS") . List.tails . reverse) $ diagonals
print . sum $ [sum forwardXMAS, sum backwardXMAS, sum downwardXMAS, sum upwardXMAS, sum diagonalXMAS, sum reverseDiagonalXMAS]
return ()
getBlock h w c y x = map (take w . drop x) . take h . drop y $ c
isXBlock b = do
let diagonal1 = collectDiagonal b 0 0
let diagonal2 = collectDiagonal (map List.reverse b) 0 0
diagonal1 `elem` ["SAM", "MAS"] && diagonal2 `elem` ["SAM", "MAS"]
part2 c = do
let lineBlocks = List.map (getBlock 3 3 c) [0..length c - 1]
let groupedBlocks = List.map (flip List.map [0..(length . head $ c) - 1]) lineBlocks
print . sum . map (length . filter isXBlock) $ groupedBlocks
return ()
main = do
c <- lines <$> getContents
part1 c
part2 c
return ()
solution
import aoc
def setup():
return (aoc.get_lines(4, padded=(True, '.', 3)), 0)
def one():
lines, acc = setup()
for row, l in enumerate(lines):
for col, c in enumerate(l):
if c == 'X':
w = l[col - 3:col + 1]
e = l[col:col + 4]
n = c + lines[row - 1][col] + \
lines[row - 2][col] + lines[row - 3][col]
s = c + lines[row + 1][col] + \
lines[row + 2][col] + lines[row + 3][col]
nw = c + lines[row - 1][col - 1] + \
lines[row - 2][col - 2] + lines[row - 3][col - 3]
ne = c + lines[row - 1][col + 1] + \
lines[row - 2][col + 2] + lines[row - 3][col + 3]
sw = c + lines[row + 1][col - 1] + \
lines[row + 2][col - 2] + lines[row + 3][col - 3]
se = c + lines[row + 1][col + 1] + \
lines[row + 2][col + 2] + lines[row + 3][col + 3]
for word in [w, e, n, s, nw, ne, sw, se]:
if word in ['XMAS', 'SAMX']:
acc += 1
print(acc)
def two():
lines, acc = setup()
for row, l in enumerate(lines):
for col, c in enumerate(l):
if c == 'A':
l = lines[row - 1][col - 1] + c + lines[row + 1][col + 1]
r = lines[row + 1][col - 1] + c + lines[row - 1][col + 1]
if l in ['MAS', 'SAM'] and r in ['MAS', 'SAM']:
acc += 1
print(acc)
one()
two()
Popular language this year :)
I got embarrassingly stuck on this one trying to be clever with list operations. Then I realized I should just use an array...
import Data.Array.Unboxed (UArray)
import Data.Array.Unboxed qualified as A
import Data.Bifunctor
readInput :: String -> UArray (Int, Int) Char
readInput s =
let rows = lines s
n = length rows
in A.listArray ((1, 1), (n, n)) $ concat rows
s1 `eq` s2 = s1 == s2 || s1 == reverse s2
part1 arr = length $ filter isXmas $ concatMap lines $ A.indices arr
where
isXmas ps = all (A.inRange $ A.bounds arr) ps && map (arr A.!) ps `eq` "XMAS"
lines p = [take 4 $ iterate (bimap (+ di) (+ dj)) p | (di, dj) <- [(1, 0), (0, 1), (1, 1), (1, -1)]]
part2 arr = length $ filter isXmas innerPoints
where
innerPoints =
let ((i1, j1), (i2, j2)) = A.bounds arr
in [(i, j) | i <- [i1 + 1 .. i2 - 1], j <- [j1 + 1 .. j2 - 1]]
isXmas p = up p `eq` "MAS" && down p `eq` "MAS"
up (i, j) = map (arr A.!) [(i + 1, j - 1), (i, j), (i - 1, j + 1)]
down (i, j) = map (arr A.!) [(i - 1, j - 1), (i, j), (i + 1, j + 1)]
main = do
input <- readInput <$> readFile "input04"
print $ part1 input
print $ part2 input
import Control.Arrow
import Data.Array.Unboxed
import Data.List
type Pos = (Int, Int)
type Board = Array Pos Char
data Dir = N | NE | E | SE | S | SW | W | NW
target = "XMAS"
parse s = listArray ((1, 1), (n, m)) [l !! i !! j | i <- [0 .. n - 1], j <- [0 .. m - 1]]
where
l = lines s
(n, m) = (length $ head l, length l)
move N = first pred
move S = first succ
move E = second pred
move W = second succ
move NW = move N . move W
move SW = move S . move W
move NE = move N . move E
move SE = move S . move E
check :: Board -> Pos -> Int -> Dir -> Bool
check b p i d =
i >= length target
|| ( inRange (bounds b) p
&& (b ! p) == (target !! i)
&& check b (move d p) (succ i) d
)
checkAllDirs :: Board -> Pos -> Int
checkAllDirs b p = length . filter (check b p 0) $ [N, NE, E, SE, S, SW, W, NW]
check2 :: Board -> Pos -> Bool
check2 b p =
all (inRange (bounds b)) moves && ((b ! p) == 'A') && ("SSMM" `elem` rotations)
where
rotations = rots $ (b !) <$> moves
moves = flip move p <$> [NE, SE, SW, NW]
rots xs = init $ zipWith (++) (tails xs) (inits xs)
part1 b = sum $ checkAllDirs b <$> indices b
part2 b = length . filter (check2 b) $ indices b
main = getContents >>= print . (part1 &&& part2) . parse
C#
public class Day04 : Solver
{
private int width, height;
private char[,] data;
public void Presolve(string input) {
var lines = input.Trim().Split("\n").ToList();
height = lines.Count;
width = lines[0].Length;
data = new char[height, width];
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
data[i, j] = lines[i][j];
}
}
}
private static readonly string word = "XMAS";
public string SolveFirst()
{
int counter = 0;
for (int start_i = 0; start_i < height; start_i++) {
for (int start_j = 0; start_j < width; start_j++) {
if (data[start_i, start_j] != word[0]) continue;
for (int di = -1; di <= 1; di++) {
for (int dj = -1; dj <= 1; dj++) {
if (di == 0 && dj == 0) continue;
int end_i = start_i + di * (word.Length - 1);
int end_j = start_j + dj * (word.Length - 1);
if (end_i < 0 || end_j < 0 || end_i >= height || end_j >= width) continue;
for (int k = 1; k < word.Length; k++) {
if (data[start_i + di * k, start_j + dj * k] != word[k]) break;
if (k == word.Length - 1) counter++;
}
}
}
}
}
return counter.ToString();
}
public string SolveSecond()
{
int counter = 0;
for (int start_i = 1; start_i < height - 1; start_i++) {
for (int start_j = 1; start_j < width - 1; start_j++) {
if (data[start_i, start_j] != 'A') continue;
int even_mas_starts = 0;
for (int di = -1; di <= 1; di++) {
for (int dj = -1; dj <= 1; dj++) {
if (di == 0 && dj == 0) continue;
if ((di + dj) % 2 != 0) continue;
if (data[start_i + di, start_j + dj] != 'M') continue;
if (data[start_i - di, start_j - dj] != 'S') continue;
even_mas_starts++;
}
}
if (even_mas_starts == 2) counter++;
}
}
return counter.ToString();
}
}
One of those with running through tricky grid indices. The vector types from the euclid crate helped in dealing with positions.
Code
use euclid::{vec2, default::*};
fn count_xmas(grid: &[&[u8]], pos: (usize, usize)) -> u32 {
if grid[pos.1][pos.0] != b'X' {
return 0
}
let bounds = Rect::new(Point2D::origin(), Size2D::new(grid[0].len() as i32, grid.len() as i32));
const DIRS: [Vector2D<i32>; 8] = [
vec2(1, 0), vec2(-1, 0), vec2(0, 1), vec2(0, -1),
vec2(1, 1), vec2(1, -1), vec2(-1, 1), vec2(-1, -1),
];
let mut count = 0;
for dir in DIRS {
let mut cur = Point2D::from(pos).to_i32();
let mut found = true;
for letter in [b'M', b'A', b'S'] {
cur += dir;
if !bounds.contains(cur) || grid[cur.y as usize][cur.x as usize] != letter {
found = false;
break
}
}
if found {
count += 1;
}
}
count
}
fn part1(input: String) {
let grid = input.lines().map(|l| l.as_bytes()).collect::<Vec<_>>();
let count = (0..grid.len()).map(|y| {
(0..grid[y].len()).map(|x| count_xmas(&grid, (x, y))).sum::<u32>()
})
.sum::<u32>();
println!("{count}");
}
fn is_x_mas(grid: &[&[u8]], pos: (usize, usize)) -> bool {
if grid[pos.1][pos.0] != b'A' {
return false
}
const DIRS: [Vector2D<i32>; 4] = [vec2(1, -1), vec2(1, 1), vec2(-1, 1), vec2(-1, -1)];
let pos = Point2D::from(pos).to_i32();
(0..4).any(|d| {
let m_pos = [pos + DIRS[d], pos + DIRS[(d + 1) % 4]]; // 2 adjacent positions of M
let s_pos = [pos + DIRS[(d + 2) % 4], pos + DIRS[(d + 3) % 4]]; // others S
m_pos.iter().all(|p| grid[p.y as usize][p.x as usize] == b'M') &&
s_pos.iter().all(|p| grid[p.y as usize][p.x as usize] == b'S')
})
}
fn part2(input: String) {
let grid = input.lines().map(|l| l.as_bytes()).collect::<Vec<_>>();
let count = (1..grid.len() - 1).map(|y| {
(1..grid[y].len() - 1).filter(|&x| is_x_mas(&grid, (x, y))).count()
})
.sum::<usize>();
println!("{count}");
}
util::aoc_main!();
(also on github)
I tried to think of some clever LINQ to do this one, but was blanking entirely.
So naïve search it is.
C#
string wordsearch = "";
int width;
int height;
public void Input(IEnumerable<string> lines)
{
wordsearch = string.Join("", lines);
height = lines.Count();
width = lines.First().Length;
}
public void Part1()
{
int words = 0;
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
words += SearchFrom(x, y);
Console.WriteLine($"Words: {words}");
}
public void Part2()
{
int words = 0;
for (int y = 1; y < height - 1; y++)
for (int x = 1; x < width - 1; x++)
words += SearchCross(x, y);
Console.WriteLine($"Crosses: {words}");
}
public int SearchFrom(int x, int y)
{
char at = wordsearch[y * width + x];
if (at != 'X')
return 0;
int words = 0;
for (int ydir = -1; ydir <= 1; ++ydir)
for (int xdir = -1; xdir <= 1; ++xdir)
{
if (xdir == 0 && ydir == 0)
continue;
if (SearchWord(x, y, xdir, ydir))
words++;
}
return words;
}
private readonly string word = "XMAS";
public bool SearchWord(int x, int y, int xdir, int ydir)
{
int wordit = 0;
while (true)
{
char at = wordsearch[y * width + x];
if (at != word[wordit])
return false;
if (wordit == word.Length - 1)
return true;
wordit++;
x += xdir;
y += ydir;
if (x < 0 || y < 0 || x >= width || y >= height)
return false;
}
}
public int SearchCross(int x, int y)
{
if (x == 0 || y == 0 || x == width - 1 || y == width - 1)
return 0;
char at = wordsearch[y * width + x];
if (at != 'A')
return 0;
int found = 0;
for (int ydir = -1; ydir <= 1; ++ydir)
for (int xdir = -1; xdir <= 1; ++xdir)
{
if (xdir == 0 || ydir == 0)
continue;
if (wordsearch[(y + ydir) * width + (x + xdir)] != 'M')
continue;
if (wordsearch[(y - ydir) * width + (x - xdir)] != 'S')
continue;
found++;
}
if (found == 2)
return 1;
return 0;
}
I haven't quite started yet, and this one does feel like a busy work kinda problem. I was wondering if I could write something to rotate the board and do the search, but I think that might be not worth the effort