263 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			263 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
// Solutions to the Advent Of Code 2024.
 | 
						|
// Copyright (C) 2025  Stefan Müller
 | 
						|
//
 | 
						|
// This program is free software: you can redistribute it and/or modify it under
 | 
						|
// the terms of the GNU General Public License as published by the Free Software
 | 
						|
// Foundation, either version 3 of the License, or (at your option) any later
 | 
						|
// version.
 | 
						|
//
 | 
						|
// This program is distributed in the hope that it will be useful, but WITHOUT
 | 
						|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 | 
						|
// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 | 
						|
//
 | 
						|
// You should have received a copy of the GNU General Public License along with
 | 
						|
// this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
						|
 | 
						|
#include <aoc/ReindeerMaze.hpp>
 | 
						|
 | 
						|
ReindeerMaze::ReindeerMaze(const int inputFileNameSuffix)
 | 
						|
    : LinesSolver{ inputFileNameSuffix }
 | 
						|
{
 | 
						|
}
 | 
						|
 | 
						|
const std::string ReindeerMaze::getPuzzleName() const
 | 
						|
{
 | 
						|
    return "Reindeer Maze";
 | 
						|
}
 | 
						|
 | 
						|
const int ReindeerMaze::getPuzzleDay() const
 | 
						|
{
 | 
						|
    return 16;
 | 
						|
}
 | 
						|
 | 
						|
void ReindeerMaze::finish()
 | 
						|
{
 | 
						|
    WeightedEdgeGraph graph{};
 | 
						|
    auto entry = graph.addVertex();
 | 
						|
    auto exit = graph.addVertex();
 | 
						|
    buildPathSegmentGraph(graph, entry, exit);
 | 
						|
 | 
						|
    part1 = graph.dijkstra(entry, exit) / 2;
 | 
						|
}
 | 
						|
 | 
						|
constexpr char ReindeerMaze::getStartChar()
 | 
						|
{
 | 
						|
    return 'S';
 | 
						|
}
 | 
						|
 | 
						|
constexpr char ReindeerMaze::getEndChar()
 | 
						|
{
 | 
						|
    return 'E';
 | 
						|
}
 | 
						|
 | 
						|
constexpr char ReindeerMaze::getWallChar()
 | 
						|
{
 | 
						|
    return '#';
 | 
						|
}
 | 
						|
 | 
						|
constexpr int ReindeerMaze::getTurnCost()
 | 
						|
{
 | 
						|
    return 1000;
 | 
						|
}
 | 
						|
 | 
						|
// Constructs the graph of path segment incidences, starting with a graph that already contains the entry and the exit
 | 
						|
// vertices.
 | 
						|
void ReindeerMaze::buildPathSegmentGraph(WeightedEdgeGraph& graph, const int entry, const int exit)
 | 
						|
{
 | 
						|
    // Uses list for work items to prevent invalidation of iterators on add.
 | 
						|
    std::list<ReindeerMazeCrossing> crossings{};
 | 
						|
    initializeWorkList(crossings, entry);
 | 
						|
 | 
						|
    while (!crossings.empty())
 | 
						|
    {
 | 
						|
        auto startCrossing{ --crossings.end() };
 | 
						|
        Point2 startPosition = startCrossing->getPosition();
 | 
						|
        auto incidence = std::find_if(startCrossing->incidences.begin(), startCrossing->incidences.end(),
 | 
						|
            [](auto& x) { return x.getPathVertex() == -1; });
 | 
						|
        Point2 direction{ incidence->getDirection() };
 | 
						|
        Point2 backwards{ -direction };
 | 
						|
        Point2 position{ startPosition + direction };
 | 
						|
 | 
						|
        int pathCost{ 1 };
 | 
						|
        bool isSkipped{ false };
 | 
						|
 | 
						|
        int nNext{ 0 };
 | 
						|
        while (getCharAt(position) != getEndChar() &&
 | 
						|
               std::find_if(crossings.begin(), crossings.end(),
 | 
						|
                   [position](auto& x) { return x.getPosition() == position; }) == crossings.end())
 | 
						|
        {
 | 
						|
            nNext = 0;
 | 
						|
            Point2 nextPosition, nextDirection;
 | 
						|
            for (const auto& checkDirection : Point2::cardinalDirections)
 | 
						|
            {
 | 
						|
                if (checkDirection != backwards)
 | 
						|
                {
 | 
						|
                    Point2 checkPosition{ checkDirection + position };
 | 
						|
                    if (getCharAt(checkPosition) != getWallChar())
 | 
						|
                    {
 | 
						|
                        nNext++;
 | 
						|
                        if (nNext == 1)
 | 
						|
                        {
 | 
						|
                            // Found a possible next step.
 | 
						|
                            nextPosition = checkPosition;
 | 
						|
                            nextDirection = checkDirection;
 | 
						|
                        }
 | 
						|
                        else if (nNext == 2)
 | 
						|
                        {
 | 
						|
                            // Found a second possible step, i.e. a new crossing. Will stop processing this path
 | 
						|
                            // segment.
 | 
						|
                            crossings.emplace_back(position);
 | 
						|
                            crossings.back().incidences.emplace_back(backwards);
 | 
						|
                            crossings.back().incidences.emplace_back(nextDirection);
 | 
						|
                            crossings.back().incidences.emplace_back(checkDirection);
 | 
						|
                        }
 | 
						|
                        else
 | 
						|
                        {
 | 
						|
                            // Found another possible step. Adds the incidence to the new crossing.
 | 
						|
                            crossings.back().incidences.emplace_back(checkDirection);
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            if (nNext == 1)
 | 
						|
            {
 | 
						|
                position = nextPosition;
 | 
						|
                pathCost++;
 | 
						|
                if (direction != nextDirection)
 | 
						|
                {
 | 
						|
                    pathCost += getTurnCost();
 | 
						|
                    direction = nextDirection;
 | 
						|
                    backwards = -direction;
 | 
						|
                }
 | 
						|
 | 
						|
                if (position == startPosition)
 | 
						|
                {
 | 
						|
                    // Stops processing this path segment because it is a loop, and remove both incidences from the
 | 
						|
                    // start crossing. Both must exist here.
 | 
						|
                    startCrossing->incidences.erase(incidence);
 | 
						|
                    auto endIncidence = std::find_if(startCrossing->incidences.begin(), startCrossing->incidences.end(),
 | 
						|
                        [backwards](auto& x) { return x.getDirection() == backwards; });
 | 
						|
                    startCrossing->incidences.erase(endIncidence);
 | 
						|
                    isSkipped = true;
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
            }
 | 
						|
            else if (nNext == 0)
 | 
						|
            {
 | 
						|
                // Stops processing this path segment because it is a dead-end, and remove it from the start crossing.
 | 
						|
                startCrossing->incidences.erase(incidence);
 | 
						|
                isSkipped = true;
 | 
						|
                break;
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                // Stops processing this path segment because a new crossing has been found. This check avoids the
 | 
						|
                // find_if() call in the loop condition.
 | 
						|
                break;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (!isSkipped)
 | 
						|
        {
 | 
						|
            // Adds the new maze path segment as a vertex.
 | 
						|
            auto pathVertex = graph.addVertex();
 | 
						|
 | 
						|
            // Updates start crossing.
 | 
						|
            incidence->setPathVertex(pathVertex);
 | 
						|
            incidence->setPathCost(pathCost);
 | 
						|
 | 
						|
            // Determines the end crossing of the new path segment and its incidence.
 | 
						|
            std::list<ReindeerMazeCrossing>::iterator endCrossing;
 | 
						|
            std::vector<ReindeerMazePathIncidence>::iterator endIncidence;
 | 
						|
            endCrossing = nNext == 1 ? endCrossing = std::find_if(crossings.begin(), crossings.end(),
 | 
						|
                                           [position](auto& x) { return x.getPosition() == position; })
 | 
						|
                                     : --crossings.end();
 | 
						|
            if (endCrossing != crossings.end())
 | 
						|
            {
 | 
						|
                // This incidence must exist, no need to check the incidence iterator.
 | 
						|
                endIncidence = std::find_if(endCrossing->incidences.begin(), endCrossing->incidences.end(),
 | 
						|
                    [backwards](auto& x) { return x.getDirection() == backwards; });
 | 
						|
                endIncidence->setPathVertex(pathVertex);
 | 
						|
                endIncidence->setPathCost(pathCost);
 | 
						|
            }
 | 
						|
 | 
						|
            // Connects the new path segment to all adjacent path segments.
 | 
						|
            AddPathSegmentEdges(graph, *incidence, startCrossing->incidences);
 | 
						|
            if (endCrossing != crossings.end())
 | 
						|
            {
 | 
						|
                AddPathSegmentEdges(graph, *endIncidence, endCrossing->incidences);
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                graph.addEdge(pathVertex, exit, pathCost);
 | 
						|
            }
 | 
						|
 | 
						|
            // Checks if end crossing is finished.
 | 
						|
            // checkFinishedCrossing(crossings, endCrossing);
 | 
						|
            if (endCrossing != crossings.end() && endCrossing->isFinished())
 | 
						|
            {
 | 
						|
                crossings.erase(endCrossing);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // Checks if start crossing is finished.
 | 
						|
        if (startCrossing->isFinished())
 | 
						|
        {
 | 
						|
            crossings.erase(startCrossing);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// Initializes the work list of crossing incidences.
 | 
						|
void ReindeerMaze::initializeWorkList(std::list<ReindeerMazeCrossing>& crossings, const int entryVertex)
 | 
						|
{
 | 
						|
    Point2 start{ findStart() };
 | 
						|
    crossings.emplace_back(start);
 | 
						|
    crossings.back().incidences.emplace_back(Point2::left, entryVertex);
 | 
						|
    addCheckedIncidence(crossings.back().incidences, start, Point2::right);
 | 
						|
    addCheckedIncidence(crossings.back().incidences, start, Point2::up);
 | 
						|
}
 | 
						|
 | 
						|
void ReindeerMaze::addCheckedIncidence(std::vector<ReindeerMazePathIncidence>& incidences, const Point2 start,
 | 
						|
    const Point2 direction)
 | 
						|
{
 | 
						|
    if (getCharAt(start + direction) != getWallChar())
 | 
						|
    {
 | 
						|
        incidences.emplace_back(direction);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
Point2 ReindeerMaze::findStart() const
 | 
						|
{
 | 
						|
    for (int j = 0; j < lines.size(); j++)
 | 
						|
    {
 | 
						|
        for (int i = 0; i < lines[j].size(); i++)
 | 
						|
        {
 | 
						|
            if (lines[j][i] == getStartChar())
 | 
						|
            {
 | 
						|
                return { i, j };
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    return { 0, 0 };
 | 
						|
}
 | 
						|
 | 
						|
void ReindeerMaze::AddPathSegmentEdges(WeightedEdgeGraph& graph, const ReindeerMazePathIncidence& pathIncidence,
 | 
						|
    const std::vector<ReindeerMazePathIncidence>& otherPathIncidences)
 | 
						|
{
 | 
						|
    for (auto& otherIncidence : otherPathIncidences)
 | 
						|
    {
 | 
						|
        if (otherIncidence.getPathVertex() > -1 && otherIncidence.getPathVertex() != pathIncidence.getPathVertex())
 | 
						|
        {
 | 
						|
            int weight{ pathIncidence.getPathCost() + otherIncidence.getPathCost() };
 | 
						|
            // Checks for turn, i.e. perpendicular directions.
 | 
						|
            if (otherIncidence.getDirection().x != -pathIncidence.getDirection().x)
 | 
						|
            {
 | 
						|
                weight += 2 * getTurnCost();
 | 
						|
            }
 | 
						|
            graph.addEdge(pathIncidence.getPathVertex(), otherIncidence.getPathVertex(), weight);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |