01020304050607080910111213141516171819202122232425

Advent of Code

2023/23

A Long Walk

in C#

by encse

Visit the Advent of Code website for the problem statement here.

Today's problem looked frightening first, because it's asking for the longest path between two points of a map. Shortest path is a no brainer with Dijkstra or whatever graph search, but I don't know about an efficient way to calculate the longest one. I have a feeling that it is somehow related to the Hamiltonian path which is NP-complete, so there might not even exists a super efficient algorithm to solve today's problem in the generic case.

But this is a puzzle, so let's get to it. I decided to convert the problem from map traversal to graph traversal first. Created nodes from the crossroads of the map (those tiles that connect 3 or more "." cells). Also assigned a node to the entry and one to the exit.

Two nodes become connected if there is a road between them. That is, they are reachable following the path in the map without visiting other crossroads in between.

This reduced the problem quite a bit. In my case it went down to about 30 nodes and 120 edges for Part 2. Part 1 is even smaller with 60 edges or so.

This graph is small enough to solve it using dynamic programming with a cache. Since we have just 30+ nodes, I represented them as powers of 2 and a set of these became a bitset stored in a long.

namespace AdventOfCode.Y2023.Day23;

using System;
using System.Collections.Generic;
using System.Numerics;
using System.Linq;
using Map = System.Collections.Generic.Dictionary<System.Numerics.Complex, char>;
using Node = long;
record Edge(Node start, Node end, int distance);

[ProblemName("A Long Walk")]
class Solution : Solver {

    // Instead of dealing with the 'map' tiles directly, we convert it to a graph.
    // Nodes: the entry tile, the exit and the crossroad tiles.
    // Edges: two nodes are connected if there is a direct path between them that 
    //        doesn't contain crossroads.
    // This reduces a problem to ~30 nodes and 120 edges for the Part 2 case
    // which can be solved using a dynamic programming approach.

    static readonly Complex Up = -Complex.ImaginaryOne;
    static readonly Complex Down = Complex.ImaginaryOne;
    static readonly Complex Left = -1;
    static readonly Complex Right = 1;
    static readonly Complex[] Dirs = [Up, Down, Left, Right];

    Dictionary<char, Complex[]> exits = new() {
        ['<'] = [Left],
        ['>'] = [Right],
        ['^'] = [Up],
        ['v'] = [Down],
        ['.'] = Dirs,
        ['#'] = []
    };

    public object PartOne(string input) => Solve(input);
    public object PartTwo(string input) => Solve(RemoveSlopes(input));

    string RemoveSlopes(string st) =>
        string.Join("", st.Select(ch => ">v<^".Contains(ch) ? '.' : ch));

    int Solve(string input) {
        var (nodes, edges) = MakeGraph(input);
        var (start, goal) = (nodes.First(), nodes.Last()); 

        // Dynamic programming using a cache, 'visited' is a bitset of 'nodes'.
        var cache = new Dictionary<(Node, long), int>();
        int LongestPath(Node node, long visited) {
            if (node == goal) {
                return 0;
            } else if ((visited & node) != 0) {
                return int.MinValue; // small enough to represent '-infinity'
            }
            var key = (node, visited);
            if (!cache.ContainsKey(key)) {
                cache[key] = edges
                    .Where(e => e.start == node)
                    .Select(e => e.distance + LongestPath(e.end, visited | node))
                    .Max();
            }
            return cache[key];
        }
        return LongestPath(start, 0); 
    }

    (Node[], Edge[]) MakeGraph(string input) {
        var map = ParseMap(input);

        // row-major order: 'entry' node comes first and 'exit' is last
        var nodePos = (
            from pos in map.Keys
            orderby pos.Imaginary, pos.Real
            where IsFree(map, pos) && !IsRoad(map, pos)
            select pos
        ).ToArray();

        var nodes = (
            from i in Enumerable.Range(0, nodePos.Length) select 1L << i
        ).ToArray();

        var edges = (
            from i in Enumerable.Range(0, nodePos.Length)
            from j in Enumerable.Range(0, nodePos.Length)
            where i != j
            let distance = Distance(map, nodePos[i], nodePos[j])
            where distance > 0
            select new Edge(nodes[i], nodes[j], distance)
        ).ToArray();

        return (nodes, edges);
    }

    // Length of the road between two crossroads; -1 if not neighbours
    int Distance(Map map, Complex crossroadA, Complex crossroadB) {
        var q = new Queue<(Complex, int)>();
        q.Enqueue((crossroadA, 0));

        var visited = new HashSet<Complex> { crossroadA };
        while (q.Any()) {
            var (pos, dist) = q.Dequeue();
            foreach (var dir in exits[map[pos]]) {
                var posT = pos + dir;
                if (posT == crossroadB) {
                    return dist + 1;
                }  else if (IsRoad(map, posT) && !visited.Contains(posT)) {
                    visited.Add(posT);
                    q.Enqueue((posT, dist + 1));
                }
            }
        }
        return -1;
    }

    bool IsFree(Map map, Complex p) =>
        map.ContainsKey(p) && map[p] != '#';

    bool IsRoad(Map map, Complex p) => 
        IsFree(map, p) && Dirs.Count(d => IsFree(map, p + d)) == 2;

    Map ParseMap(string input) {
        var lines = input.Split('\n');
        return (
            from irow in Enumerable.Range(0, lines.Length)
            from icol in Enumerable.Range(0, lines[0].Length)
            let pos = new Complex(icol, irow)
            select new KeyValuePair<Complex, char>(pos, lines[irow][icol])
        ).ToDictionary();
    }
}

Please ☆ my repo if you like it!

© 2025 Advent of Code is a registered trademark in the US Images provided by Bing image creator