Skip Navigation

๐Ÿฎ - 2024 DAY 10 SOLUTIONS - ๐Ÿฎ

Day 10: Hoof It

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

32 comments
  • Haskell

    A nice easy one today: didn't even have to hit this with the optimization hammer.

     undefined
        
    import Data.Char
    import Data.List
    import Data.Map (Map)
    import Data.Map qualified as Map
    
    readInput :: String -> Map (Int, Int) Int
    readInput s =
      Map.fromList
        [ ((i, j), digitToInt c)
          | (i, l) <- zip [0 ..] (lines s),
            (j, c) <- zip [0 ..] l
        ]
    
    findTrails :: Map (Int, Int) Int -> [[[(Int, Int)]]]
    findTrails input =
      Map.elems . Map.map (filter ((== 10) . length)) $
        Map.restrictKeys accessible starts
      where
        starts = Map.keysSet . Map.filter (== 0) $ input
        accessible = Map.mapWithKey getAccessible input
        getAccessible (i, j) h
          | h == 9 = [[(i, j)]]
          | otherwise =
              [ (i, j) : path
                | (di, dj) <- [(-1, 0), (0, 1), (1, 0), (0, -1)],
                  let p = (i + di, j + dj),
                  input Map.!? p == Just (succ h),
                  path <- accessible Map.! p
              ]
    
    main = do
      trails <- findTrails . readInput <$> readFile "input10"
      mapM_
        (print . sum . (`map` trails))
        [length . nub . map last, length]
    
      
  • Nim

    As many others today, I've solved part 2 first and then fixed a 'bug' to solve part 1. =)

     nim
        
    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
    
    
      

    Codeberg Repo

  • Python

    Not surprisingly, trees

     undefined
        
    import numpy as np
    from pathlib import Path
    
    cwd = Path(__file__).parent
    
    cross = np.array([[-1,0],[1,0],[0,-1],[0,1]])
    
    class Node():
      def __init__(self, coord, parent):
        self.coord = coord
        self.parent = parent
    
      def __repr__(self):
        return f"{self.coord}"
    
    def parse_input(file_path):
    
      with file_path.open("r") as fp:
        data = list(map(list, fp.read().splitlines()))
    
      return np.array(data, dtype=int)
    
    def find_neighbours(node_pos, grid):
    
      I = list(filter(lambda x: all([c>=0 and o-c>0 for c,o in zip(x,grid.shape)]),
                      list(cross + node_pos)))
    
      candidates = grid[tuple(np.array(I).T)]
      J = np.argwhere(candidates-grid[tuple(node_pos)]==1).flatten()
    
      return list(np.array(I).T[:, J].T)
    
    def construct_tree_paths(grid):
    
      roots = list(np.argwhere(grid==0))
      trees = []
    
      for root in roots:
    
        levels = [[Node(root, None)]]
        while len(levels[-1])>0 or len(levels)==1:
          levels.append([Node(node, root) for root in levels[-1] for node in
                         find_neighbours(root.coord, grid)])
        trees.append(levels)
    
      return trees
    
    def trace_back(trees, grid):
    
      paths = []
    
      for levels in trees:
        for node in levels[-2]:
    
          path = ""
          while node is not None:
            coord = ",".join(node.coord.astype(str))
            path += f"{coord} "
            node = node.parent
          paths.append(path)
    
      return paths
    
    def solve_problem(file_name):
    
      grid = parse_input(Path(cwd, file_name))
      trees = construct_tree_paths(grid)
      trails = trace_back(trees, grid)
      ntrails = len(set(trails))
      nreached = sum([len(set([tuple(x.coord) for x in levels[-2]])) for levels in trees])
    
      return nreached, ntrails
    
    
      
  • Uiua

    Run it here!

    How to read this

    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), .

     undefined
        
    Data   โ† โŠœโ‰กโ‹•โŠธโ‰ @\n"89010123\n78121874\n87430965\n96549874\n45678903\n32019012\n01329801\n10456732"
    Nโ‚„     โ† โ‰ก+[0_1 1_0 0_ยฏ1 ยฏ1_0]ยค
    Ns     โ† โ–ฝ:โŸœ(=1-:โˆฉ(โฌš0โŠก:Data))โ–ฝโŠธโ‰ก(/ร—โ‰ฅ0)Nโ‚„. # Valid, in-bounds neighbours.
    Count! โ† /+โ‰ก(โงป^0โŠ™โ—Œ path(Ns|(=9โŠก:Data)|1))โŠš=0Data
    &p Count!(โ—ดโ‰กโ—‡โŠฃ)
    &p Count!โˆ˜
    
      
  • Rust

    Definitely a nice and easy one, I accidentally solved part 2 first, because I skimmed the challenge and missed the unique part.

     rust
        
    #[cfg(test)]
    mod tests {
    
        const DIR_ORDER: [(i8, i8); 4] = [(-1, 0), (0, 1), (1, 0), (0, -1)];
    
        fn walk_trail(board: &Vec<Vec<i8>>, level: i8, i: i8, j: i8) -> Vec<(i8, i8)> {
            let mut paths = vec![];
            if i < 0 || j < 0 {
                return paths;
            }
            let actual_level = match board.get(i as usize) {
                None => return paths,
                Some(line) => match line.get(j as usize) {
                    None => return paths,
                    Some(c) => c,
                },
            };
            if *actual_level != level {
                return paths;
            }
            if *actual_level == 9 {
                return vec![(i, j)];
            }
    
            for dir in DIR_ORDER.iter() {
                paths.extend(walk_trail(board, level + 1, i + dir.0, j + dir.1));
            }
            paths
        }
    
        fn count_unique(p0: &Vec<(i8, i8)>) -> u32 {
            let mut dedup = vec![];
            for p in p0.iter() {
                if !dedup.contains(p) {
                    dedup.push(*p);
                }
            }
            dedup.len() as u32
        }
    
        #[test]
        fn day10_part1_test() {
            let input = std::fs::read_to_string("src/input/day_10.txt").unwrap();
    
            let board = input
                .trim()
                .split('\n')
                .map(|line| {
                    line.chars()
                        .map(|c| {
                            if c == '.' {
                                -1
                            } else {
                                c.to_digit(10).unwrap() as i8
                            }
                        })
                        .collect::<Vec<i8>>()
                })
                .collect::<Vec<Vec<i8>>>();
    
            let mut total = 0;
    
            for (i, row) in board.iter().enumerate() {
                for (j, pos) in row.iter().enumerate() {
                    if *pos == 0 {
                        let all_trails = walk_trail(&board, 0, i as i8, j as i8);
                        total += count_unique(&all_trails);
                    }
                }
            }
    
            println!("{}", total);
        }
        #[test]
        fn day10_part2_test() {
            let input = std::fs::read_to_string("src/input/day_10.txt").unwrap();
    
            let board = input
                .trim()
                .split('\n')
                .map(|line| {
                    line.chars()
                        .map(|c| {
                            if c == '.' {
                                -1
                            } else {
                                c.to_digit(10).unwrap() as i8
                            }
                        })
                        .collect::<Vec<i8>>()
                })
                .collect::<Vec<Vec<i8>>>();
    
            let mut total = 0;
    
            for (i, row) in board.iter().enumerate() {
                for (j, pos) in row.iter().enumerate() {
                    if *pos == 0 {
                        total += walk_trail(&board, 0, i as i8, j as i8).len();
                    }
                }
            }
    
            println!("{}", total);
        }
    }
    
      
  • Raku

    Pretty straight-forward problem today.

     undefined
        
    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";
    }
    
      
  • Julia

    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.

  • C#

     undefined
        
    using System.Diagnostics;
    using Common;
    
    namespace Day10;
    
    static class Program
    {
        static void Main()
        {
            var start = Stopwatch.GetTimestamp();
    
            var sampleInput = Input.ParseInput("sample.txt");
            var programInput = Input.ParseInput("input.txt");
    
            Console.WriteLine($"Part 1 sample: {Part1(sampleInput)}");
            Console.WriteLine($"Part 1 input: {Part1(programInput)}");
    
            Console.WriteLine($"Part 2 sample: {Part2(sampleInput)}");
            Console.WriteLine($"Part 2 input: {Part2(programInput)}");
    
            Console.WriteLine($"That took about {Stopwatch.GetElapsedTime(start)}");
        }
    
        static object Part1(Input i) => GetTrailheads(i)
            .Sum(th => CountTheNines(th, i, new HashSet<Point>(), false));
    
        static object Part2(Input i) => GetTrailheads(i)
            .Sum(th => CountTheNines(th, i, new HashSet<Point>(), true));
    
        static int CountTheNines(Point loc, Input i, ISet<Point> visited, bool allPaths)
        {
            if (!visited.Add(loc)) return 0;
            
            var result =
                (ElevationAt(loc, i) == 9) ? 1 :
                loc.GetCardinalMoves()
                    .Where(move => move.IsInBounds(i.Bounds.Row, i.Bounds.Col))
                    .Where(move => (ElevationAt(move, i) - ElevationAt(loc, i)) == 1)
                    .Where(move => !visited.Contains(move))
                    .Sum(move => CountTheNines(move, i, visited, allPaths));
            
            if(allPaths) visited.Remove(loc);
            
            return result;
        }
    
        static IEnumerable<Point> GetTrailheads(Input i) => Grid.EnumerateAllPoints(i.Bounds)
            .Where(loc => ElevationAt(loc, i) == 0);
    
        static int ElevationAt(Point p, Input i) => i.Map[p.Row][p.Col];
    }
    
    public class Input
    {
        public required Point Bounds { get; init; }
        public required int[][] Map { get; init; }
        
        public static Input ParseInput(string file)
        {
            using var reader = new StreamReader(file);
            var map = reader.EnumerateLines()
                .Select(l => l.Select(c => (int)(c - '0')).ToArray())
                .ToArray();
            var bounds = new Point(map.Length, map.Max(l => l.Length));
            return new Input()
            {
                Map = map,
                Bounds = bounds,
            };
        }
    }
    
      
    • 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.

  • Haskell

    Cool task, nothing to optimize

     haskell
        
    import Control.Arrow
    
    import Data.Array.Unboxed (UArray)
    import Data.Set (Set)
    
    import qualified Data.Char as Char
    import qualified Data.List as List
    import qualified Data.Set as Set
    import qualified Data.Array.Unboxed as UArray
    
    parse :: String -> UArray (Int, Int) Int
    parse s = UArray.listArray ((1, 1), (n, m)) . map Char.digitToInt . filter (/= '\n') $ s
            where
                    n = takeWhile (/= '\n') >>> length $ s
                    m = filter (== '\n') >>> length >>> pred $ s
    
    reachableNeighbors :: (Int, Int) -> UArray (Int, Int) Int -> [(Int, Int)]
    reachableNeighbors p@(py, px) a = List.filter (UArray.inRange (UArray.bounds a))
            >>> List.filter ((a UArray.!) >>> pred >>> (== (a UArray.! p)))
            $ [(py-1, px), (py+1, px), (py, px-1), (py, px+1)]
    
    distinctTrails :: (Int, Int) -> UArray (Int, Int) Int -> Int
    distinctTrails p a
            | a UArray.! p == 9 = 1
            | otherwise = flip reachableNeighbors a
                    >>> List.map (flip distinctTrails a)
                    >>> sum
                    $ p
    
    reachableNines :: (Int, Int) -> UArray (Int, Int) Int -> Set (Int, Int)
    reachableNines p a
            | a UArray.! p == 9 = Set.singleton p
            | otherwise = flip reachableNeighbors a
                    >>> List.map (flip reachableNines a)
                    >>> Set.unions
                    $ p
    
    findZeros = UArray.assocs
            >>> filter (snd >>> (== 0))
            >>> map fst
    
    part1 a = findZeros
            >>> map (flip reachableNines a)
            >>> map Set.size
            >>> sum
            $ a
    part2 a = findZeros
            >>> map (flip distinctTrails a)
            >>> sum
            $ a
    
    main = getContents
            >>= print
            . (part1 &&& part2)
            . parse
    
      
  • 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.

  • Rust

    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.

    Also on github

  • Uiua

    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.

    Run with example input here

    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.

     uiua
        
    $ 89010123
    $ 78121874
    $ 87430965
    $ 96549874
    $ 45678903
    $ 32019012
    $ 01329801
    $ 10456732
    .
    Adj โ† ยค[0_ยฏ1 0_1 ยฏ1_0 1_0]
    
    Trails! โ† (
      โŠš=0.
      โŠ™ยค
      โ‰ก(โ–กยค)
      1
      โฅ(โŠ™(โ‰ก(โ–ก^0/โŠ‚โ‰ก(+ยค)โŠ™ยคยฐโ–ก)โŠ™Adj
          โ‰ก(โ–กโ–ฝยฌโ‰ก/++โŠƒ=โ‚‹โ‚=โ‚ˆ.ยฐโ–ก))
        +1โŸœโŠธโš(โ–ฝ=โŠ™(:โŸœโŠก))
      )9
      โŠ™โ—Œโ—Œ
      โงป/โ—‡โŠ‚
    )
    
    PartOne โ† (
      # &rs โˆž &fo "input-10.txt"
      โŠœโˆตโ‹•โ‰ @\n.
      Trails!โ—ด
    )
    
    PartTwo โ† (
      # &rs โˆž &fo "input-10.txt"
      โŠœโˆตโ‹•โ‰ @\n.
      Trails!โˆ˜
    )
    
    &p "Day 10:"
    &pf "Part 1: "
    &p PartOne
    &pf "Part 2: "
    &p PartTwo
    
      
  • C#

     undefined
        
    using QuickGraph;
    using QuickGraph.Algorithms.Search;
    using Point = (int, int);
    
    public class Day10 : Solver
    {
      private int[][] data;
      private int width, height;
      private List<int> destinations_counts = [], paths_counts = [];
      private record PointEdge(Point Source, Point Target): IEdge<Point>;
    
      private DelegateVertexAndEdgeListGraph<Point, PointEdge> MakeGraph() => new(AllPoints(), GetNeighbours);
    
      private static readonly List<Point> directions = [(1, 0), (-1, 0), (0, 1), (0, -1)];
    
      private bool GetNeighbours(Point from, out IEnumerable<PointEdge> result) {
        List<PointEdge> neighbours = [];
        int next_value = data[from.Item2][from.Item1] + 1;
        foreach (var (dx, dy) in directions) {
          int x = from.Item1 + dx, y = from.Item2 + dy;
          if (x < 0 || y < 0 || x >= width || y >= height) continue;
          if (data[y][x] != next_value) continue;
          neighbours.Add(new(from, (x, y)));
        }
        result = neighbours;
        return true;
      }
    
      private IEnumerable<Point> AllPoints() => Enumerable.Range(0, width).SelectMany(x => Enumerable.Range(0, height).Select(y => (x, y)));
    
      public void Presolve(string input) {
        data = input.Trim().Split("\n").Select(s => s.Select(ch => ch - '0').ToArray()).ToArray();
        width = data[0].Length;
        height = data.Length;
        var graph = MakeGraph();
        for (int i = 0; i < width; i++) {
          for (int j = 0; j < height; j++) {
            if (data[j][i] != 0) continue;
            var search = new BreadthFirstSearchAlgorithm<Point, PointEdge>(graph);
            Point start = (i, j);
            Dictionary<Point, int> paths_into = [];
            paths_into[start] = 1;
            var destinations = 0;
            var paths = 0;
            search.ExamineEdge += edge => {
              paths_into.TryAdd(edge.Target, 0);
              paths_into[edge.Target] += paths_into[edge.Source];
            };
            search.FinishVertex += vertex => {
              if (data[vertex.Item2][vertex.Item1] == 9) {
                paths += paths_into[vertex];
                destinations += 1;
              }
            };
            search.SetRootVertex(start);
            search.Compute();
            destinations_counts.Add(destinations);
            paths_counts.Add(paths);
          }
        }
      }
    
      public string SolveFirst() => destinations_counts.Sum().ToString();
      public string SolveSecond() => paths_counts.Sum().ToString();
    }
    
    
      
  • Dart

    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...

     undefined
        
    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);
    
      
32 comments