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
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
Tried a dynamic programming kind of thing first but recursion suited the problem much better.
Part 2 seemed incompatible with my visited list representation. Then at the office I suddenly realised I just had to skip a single if(). Funny how that works when you let things brew in the back of your mind.
Code
#include "common.h"
#define GZ 43
/*
* To avoid having to clear the 'seen' array after every search we mark
* and check it with a per-search marker value ('id').
*/
static char g[GZ][GZ];
static int seen[GZ][GZ];
static int
score(int id, int x, int y, int p2)
{
if (x<0 || x>=GZ ||
y<0 || y>=GZ || (!p2 && seen[y][x] == id))
return 0;
seen[y][x] = id;
if (g[y][x] == '9')
return 1;
return
(g[y-1][x] == g[y][x]+1 ? score(id, x, y-1, p2) : 0) +
(g[y+1][x] == g[y][x]+1 ? score(id, x, y+1, p2) : 0) +
(g[y][x-1] == g[y][x]+1 ? score(id, x-1, y, p2) : 0) +
(g[y][x+1] == g[y][x]+1 ? score(id, x+1, y, p2) : 0);
}
int
main(int argc, char **argv)
{
int p1=0,p2=0, id=1, x,y;
if (argc > 1)
DISCARD(freopen(argv[1], "r", stdin));
for (y=0; y<GZ && fgets(g[y], GZ, stdin); y++)
;
for (y=0; y<GZ; y++)
for (x=0; x<GZ; x++)
if (g[y][x] == '0') {
p1 += score(id++, x, y, 0);
p2 += score(id++, x, y, 1);
}
printf("10: %d %d\n", p1, p2);
return 0;
}
Uiua has a very helpful path function built in which returns all valid paths that match your criteria (using diijkstra/a* depending on whether third function is provided), making a lot of path-finding stuff almost painfully simple, as you just need to provide a starting node and three functions: return next nodes, return confirmation if we've reached a suitable target node (here testing if it's = 9), (optional) return heuristic cost to destination (here set to constant 1), .
As many others today, I've solved part 2 first and then fixed a 'bug' to solve part 1. =)
type Vec2 = tuple[x,y:int]
const Adjacent = [(x:1,y:0),(-1,0),(0,1),(0,-1)]
proc path(start: Vec2, grid: seq[string]): tuple[ends, trails: int] =
var queue = @[@[start]]
var endNodes: HashSet[Vec2]
while queue.len > 0:
let path = queue.pop()
let head = path[^1]
let c = grid[head.y][head.x]
if c == '9':
inc result.trails
endNodes.incl head
continue
for d in Adjacent:
let nd = (x:head.x + d.x, y:head.y + d.y)
if nd.x < 0 or nd.y < 0 or nd.x > grid[0].high or nd.y > grid.high:
continue
if grid[nd.y][nd.x].ord - c.ord != 1: continue
queue.add path & nd
result.ends = endNodes.len
proc solve(input: string): AOCSolution[int, int] =
let grid = input.splitLines()
var trailstarts: seq[Vec2]
for y, line in grid:
for x, c in line:
if c == '0':
trailstarts.add (x,y)
for start in trailstarts:
let (ends, trails) = start.path(grid)
result.part1 += ends
result.part2 += trails
This was a nice one. Basically 9 rounds of Breadth-First-Search, which could be neatly expressed using fold. The only difference between part 1 and part 2 turned out to be the datastructure for the search frontier: The HashSet in part 1 unifies paths as they join back to the same node, the Vec in part 2 keeps all paths separate.
Solution
use std::collections::HashSet;
fn parse(input: &str) -> Vec<&[u8]> {
input.lines().map(|l| l.as_bytes()).collect()
}
fn adj(grid: &[&[u8]], (x, y): (usize, usize)) -> Vec<(usize, usize)> {
let n = grid[y][x];
let mut adj = Vec::with_capacity(4);
if x > 0 && grid[y][x - 1] == n + 1 {
adj.push((x - 1, y))
}
if y > 0 && grid[y - 1][x] == n + 1 {
adj.push((x, y - 1))
}
if x + 1 < grid[0].len() && grid[y][x + 1] == n + 1 {
adj.push((x + 1, y))
}
if y + 1 < grid.len() && grid[y + 1][x] == n + 1 {
adj.push((x, y + 1))
}
adj
}
fn solve(input: String, trailhead: fn(&[&[u8]], (usize, usize)) -> u32) -> u32 {
let grid = parse(&input);
let mut sum = 0;
for (y, row) in grid.iter().enumerate() {
for (x, p) in row.iter().enumerate() {
if *p == b'0' {
sum += trailhead(&grid, (x, y));
}
}
}
sum
}
fn part1(input: String) {
fn score(grid: &[&[u8]], start: (usize, usize)) -> u32 {
(1..=9)
.fold(HashSet::from([start]), |frontier, _| {
frontier.iter().flat_map(|p| adj(grid, *p)).collect()
})
.len() as u32
}
println!("{}", solve(input, score))
}
fn part2(input: String) {
fn rating(grid: &[&[u8]], start: (usize, usize)) -> u32 {
(1..=9)
.fold(vec![start], |frontier, _| {
frontier.iter().flat_map(|p| adj(grid, *p)).collect()
})
.len() as u32
}
println!("{}", solve(input, rating))
}
util::aoc_main!();
Nice to have a really simple one for a change, both my day 1 and 2 solutions worked on their very first attempts.
I rewrote the code to combine the two though, since the implementations were almost identical for both solutions, and also to replace the recursion with a search list instead.
C#
int[] heights = new int[0];
(int, int) size = (0, 0);
public void Input(IEnumerable<string> lines)
{
size = (lines.First().Length, lines.Count());
heights = string.Concat(lines).Select(c => int.Parse(c.ToString())).ToArray();
}
int trails = 0, trailheads = 0;
public void PreCalc()
{
for (int y = 0; y < size.Item2; ++y)
for (int x = 0; x < size.Item1; ++x)
if (heights[y * size.Item1 + x] == 0)
{
var unique = new HashSet<(int, int)>();
trails += CountTrails((x, y), unique);
trailheads += unique.Count;
}
}
public void Part1()
{
Console.WriteLine($"Trailheads: {trailheads}");
}
public void Part2()
{
Console.WriteLine($"Trails: {trails}");
}
int CountTrails((int, int) from, HashSet<(int,int)> unique)
{
int found = 0;
List<(int,int)> toSearch = new List<(int, int)>();
toSearch.Add(from);
while (toSearch.Any())
{
var cur = toSearch.First();
toSearch.RemoveAt(0);
int height = heights[cur.Item2 * size.Item1 + cur.Item1];
for (int y = -1; y <= 1; ++y)
for (int x = -1; x <= 1; ++x)
{
if ((y != 0 && x != 0) || (y == 0 && x == 0))
continue;
var newAt = (cur.Item1 + x, cur.Item2 + y);
if (newAt.Item1 < 0 || newAt.Item1 >= size.Item1 || newAt.Item2 < 0 || newAt.Item2 >= size.Item2)
continue;
int newHeight = heights[newAt.Item2 * size.Item1 + newAt.Item1];
if (newHeight - height != 1)
continue;
if (newHeight == 9)
{
unique.Add(newAt);
found++;
continue;
}
toSearch.Add(newAt);
}
}
return found;
}
Yes. I don't know whether this is a beehaw specific issue (that being my home instance) or a lemmy issue in general, but < and & are HTML escaped in all code blocks I see. Of course, this is substantially more painful for J code than many other languages.
Quite happy that today went a lot smoother than yesterday even though I am not really familiar with recursion. Normally I never use recursion but I felt like today could be solved by it (or using trees, but I'm even less familiar with them). Surprisingly my solution actually worked and for part 2 only small modifications were needed to count peaks reached by each trail.
Code
function readInput(inputFile::String)
f = open(inputFile,"r")
lines::Vector{String} = readlines(f)
close(f)
topoMap = Matrix{Int}(undef,length(lines),length(lines[1]))
for (i,l) in enumerate(lines)
topoMap[i,:] = map(x->parse(Int,x),collect(l))
end
return topoMap
end
function getTrailheads(topoMap::Matrix{Int})
trailheads::Vector{Vector{Int}} = []
for (i,r) in enumerate(eachrow(topoMap))
for (j,c) in enumerate(r)
c==0 ? push!(trailheads,[i,j]) : nothing
end
end
return trailheads
end
function getReachablePeaks(topoMap::Matrix{Int},trailheads::Vector{Vector{Int}})
reachablePeaks = Dict{Int,Vector{Vector{Int}}}()
function getPossibleMoves(topoMap::Matrix{Int},pos::Vector{Int})
possibleMoves::Vector{Vector{Int}} = []
pos[1]-1 in 1:size(topoMap)[1] && topoMap[pos[1]-1,pos[2]]==topoMap[pos[1],pos[2]]+1 ? push!(possibleMoves,[pos[1]-1,pos[2]]) : nothing #up?
pos[1]+1 in 1:size(topoMap)[1] && topoMap[pos[1]+1,pos[2]]==topoMap[pos[1],pos[2]]+1 ? push!(possibleMoves,[pos[1]+1,pos[2]]) : nothing #down?
pos[2]-1 in 1:size(topoMap)[2] && topoMap[pos[1],pos[2]-1]==topoMap[pos[1],pos[2]]+1 ? push!(possibleMoves,[pos[1],pos[2]-1]) : nothing #left?
pos[2]+1 in 1:size(topoMap)[2] && topoMap[pos[1],pos[2]+1]==topoMap[pos[1],pos[2]]+1 ? push!(possibleMoves,[pos[1],pos[2]+1]) : nothing #right?
return possibleMoves
end
function walkPossMoves(topoMap::Matrix{Int},pos::Vector{Int},reachedPeaks::Matrix{Bool},trailId::Int)
possMoves::Vector{Vector{Int}} = getPossibleMoves(topoMap,pos)
for m in possMoves
if topoMap[m[1],m[2]]==9
reachedPeaks[m[1],m[2]]=1
trailId += 1
continue
end
reachedPeaks,trailId = walkPossMoves(topoMap,m,reachedPeaks,trailId)
end
return reachedPeaks, trailId
end
peaksScore::Int = 0; trailsScore::Int = 0
trailId::Int = 0
for (i,t) in enumerate(trailheads)
if !haskey(reachablePeaks,i); reachablePeaks[i]=[]; end
reachedPeaks::Matrix{Bool} = zeros(size(topoMap))
trailId = 0
reachedPeaks,trailId = walkPossMoves(topoMap,t,reachedPeaks,trailId)
trailPeaksScore = sum(reachedPeaks)
peaksScore += trailPeaksScore
trailsScore += trailId
end
return peaksScore,trailsScore
end #getReachablePeaks
topoMap::Matrix{Int} = readInput("input/day10Input")
trailheads::Vector{Vector{Int}} = getTrailheads(topoMap)
@info "Part 1"
reachablePeaks = getReachablePeaks(topoMap,trailheads)[1]
println("reachable peaks: ",reachablePeaks)
@info "Part 2"
trailsScore::Int = getReachablePeaks(topoMap,trailheads)[2]
println("trails score: $trailsScore")
sub MAIN($input) {
my $file = open $input;
my @map = $file.slurp.trim.lines>>.comb>>.Int;
my @pos-tracking = [] xx 10;
for 0..^@map.elems X 0..^@map[0].elems -> ($row, $col) {
@pos-tracking[@map[$row][$col]].push(($row, $col).List);
}
my %on-possible-trail is default([]);
my %trail-score-part2 is default(0);
for 0..^@pos-tracking.elems -> $height {
for @pos-tracking[$height].List -> ($row, $col) {
if $height == 0 {
%on-possible-trail{"$row;$col"} = set ("$row;$col",);
%trail-score-part2{"$row;$col"} = 1;
} else {
for ((1,0), (-1, 0), (0, 1), (0, -1)) -> @neighbor-direction {
my @neighbor-position = ($row, $col) Z+ @neighbor-direction;
next if @neighbor-position.any < 0 or (@neighbor-position Z>= (@map.elems, @map[0].elems)).any;
next if @map[@neighbor-position[0]][@neighbor-position[1]] != $height - 1;
%on-possible-trail{"$row;$col"} โช= %on-possible-trail{"{@neighbor-position[0]};{@neighbor-position[1]}"};
%trail-score-part2{"$row;$col"} += %trail-score-part2{"{@neighbor-position[0]};{@neighbor-position[1]}"};
}
}
}
}
my $part1-solution = @pos-tracking[9].map({%on-possible-trail{"{$_[0]};{$_[1]}"}.elems}).sum;
say "part 1: $part1-solution";
my $part2-solution = @pos-tracking[9].map({%trail-score-part2{"{$_[0]};{$_[1]}"}}).sum;
say "part 2: $part2-solution";
}
Straightforward depth first search. I found that the only difference for part 2 was to remove the current location from the HashSet of visited locations when the recurive call finished so that it could be visited again in other unique paths.
After finally deciding to put aside Day 9 Part 2 for now, this was really easy actually. The longest was figuring out how many extra dimensions I had to give some arrays and where to remove those again (and how). Then part 2 came along and all I had to do was remove a single character (not removing duplicates when landing on the same field by going different ways from the same starting point). Basically, everything in the parentheses of the Trails! macro was my solution for part 1, just that the ^0 was โด (deduplicate). Once that was removed, the solution for part 2 was there as well.
Note: in order to use the code here for the actual input, you have to replace =โ with =โ โ because I was too lazy to make it work with variable array sizes this time.
Maaaannnn. Today's solution was really something. I actually got so confused initially that I unknowingly wrote the algorithm for part 2 before I even finished part 1! As an upside, however, I did expand my own Advent of Code standard library ;)
Sets of tuples and iteration for both first and second parts. A list of tuples used as a stack for the conversion of recursion to iteration. Dictionary of legal trail moves for traversal. Type hints for antibugging in VSCode. Couple of seconds runtime for each part.
Not "debugging" ... the value comes before I even try to run the code. The background syntax checker highlights when the types don't agree into and out of each function call and I don't get errors like trying to index into an integer.
As for time... I guessed... I did not measure. I have limited time to play with this and don't optimize unless I find myself waiting excessively for an answer.
I dug out the hill-climbing trail-walking code from 2022 Day 12, put it up on blocks, and stripped all the weirdness out of it.
Ended up with just a simple flood-fill from each trailhead, so it turns out I only actually used the Graph and Node classes which I then also stripped out...
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:more/more.dart';
late Map<Point, int> nodes;
late Map<Point, List> nexts;
/// Parse the lines, build up nodes and nexts, return starts.
List<Point> parse(ls) {
nodes = {
for (var y in 0.to(ls.length))
for (var x in 0.to(ls.first.length)) Point(x, y): int.parse(ls[y][x])
};
nexts = Map.fromEntries(nodes.keys.map((n) => MapEntry(
n,
[Point(0, 1), Point(0, -1), Point(1, 0), Point(-1, 0)]
.map((d) => n + d)
.where((d) => (nodes[d] ?? -1) - nodes[n]! == 1)
.toList())));
return nodes.keys.where((e) => nodes[e] == 0).toList();
}
/// Given a starting node, return all valid paths to any '9' on the grid.
Set paths(Point here, [Set sofar = const {}]) {
if (nodes[here]! == 9) return {sofar};
return nexts[here]!
.where((e) => !sofar.contains(e))
.fold({}, (s, f) => s..addAll(paths(f, sofar.toSet()..add(f))));
}
/// Finds all paths from each start, then apply the fn to each and sum.
count(lines, int Function(Set) fn) => parse(lines).map(paths).map<int>(fn).sum;
part1(lines) => count(lines, (e) => e.map((p) => p.last).toSet().length);
part2(lines) => count(lines, (e) => e.length);