|
|
|
@ -0,0 +1,249 @@
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using AdventOfCode.Common;
|
|
|
|
|
using AdventOfCode.Models;
|
|
|
|
|
|
|
|
|
|
namespace AdventOfCode._2022.Day12
|
|
|
|
|
{
|
|
|
|
|
public class Heightmap
|
|
|
|
|
{
|
|
|
|
|
public int TotalSteps { get; set; }
|
|
|
|
|
public string[] Steps { get; set; }
|
|
|
|
|
private Coord _start, _end;
|
|
|
|
|
private char[,] _map;
|
|
|
|
|
private readonly int MaxX, MaxY;
|
|
|
|
|
public Heightmap(string[] input)
|
|
|
|
|
{
|
|
|
|
|
//Create our 2d board
|
|
|
|
|
_map = new char[input.Count(), input.First().Length];
|
|
|
|
|
for (int i = 0; i < input.Count(); i++)
|
|
|
|
|
{
|
|
|
|
|
var line = input[i];
|
|
|
|
|
for (int j = 0; j < line.Length; j++)
|
|
|
|
|
{
|
|
|
|
|
_map[i, j] = line[j];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Find the start and end
|
|
|
|
|
MaxX = _map.GetLength(0);
|
|
|
|
|
MaxY = _map.GetLength(1);
|
|
|
|
|
for (int x = 0; x < _map.GetLength(0); x++)
|
|
|
|
|
{
|
|
|
|
|
for (int y = 0; y < _map.GetLength(1); y++)
|
|
|
|
|
{
|
|
|
|
|
if (_map[x, y] == 'S') _start = new Coord(x, y);
|
|
|
|
|
if (_map[x, y] == 'E') _end = new Coord(x, y);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Ensure a start and end was found
|
|
|
|
|
if (_start == null) throw new ArgumentException("No start coord found!");
|
|
|
|
|
if (_end == null) throw new ArgumentException("No end coord found!");
|
|
|
|
|
_start.SetDistance(_end);//Set the distance between start and end
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Traverse(AOCVersion version)
|
|
|
|
|
{
|
|
|
|
|
if (version == AOCVersion.A)
|
|
|
|
|
{
|
|
|
|
|
//Traverse our 2d matrix
|
|
|
|
|
AStarTraversal();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_map[_start.X, _start.Y] = 'a'; //Change our S to an 'a' elevation
|
|
|
|
|
var possibleStarts = new List<Coord>();
|
|
|
|
|
for (int x = 0; x < _map.GetLength(0); x++)
|
|
|
|
|
{
|
|
|
|
|
for (int y = 0; y < _map.GetLength(1); y++)
|
|
|
|
|
{
|
|
|
|
|
if (_map[x, y] == 'a') possibleStarts.Add(new Coord(x, y));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var routes = new List<string[]>();
|
|
|
|
|
foreach (var possibleStart in possibleStarts)
|
|
|
|
|
{
|
|
|
|
|
_start = possibleStart;
|
|
|
|
|
AStarTraversal();
|
|
|
|
|
routes.Add(Steps.ToArray());
|
|
|
|
|
}
|
|
|
|
|
Steps = routes.OrderByDescending(x => x.Length).Last();
|
|
|
|
|
TotalSteps = Steps.Length - 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private List<Coord> GetAdjacentCoords(Coord currentCoord)
|
|
|
|
|
{
|
|
|
|
|
var adjCoords = new List<Coord>()
|
|
|
|
|
{
|
|
|
|
|
new Coord { X = currentCoord.X, Y = currentCoord.Y - 1, Parent = currentCoord, Cost = currentCoord.Cost + 1 }, //Left
|
|
|
|
|
new Coord { X = currentCoord.X, Y = currentCoord.Y + 1, Parent = currentCoord, Cost = currentCoord.Cost + 1},// Right
|
|
|
|
|
new Coord { X = currentCoord.X - 1, Y = currentCoord.Y, Parent = currentCoord, Cost = currentCoord.Cost + 1 }, //Up
|
|
|
|
|
new Coord { X = currentCoord.X + 1, Y = currentCoord.Y, Parent = currentCoord, Cost = currentCoord.Cost + 1 }, //Down
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
adjCoords.ForEach(coord => coord.SetDistance(_end));
|
|
|
|
|
var currentChar = _map[currentCoord.X, currentCoord.Y];
|
|
|
|
|
if (currentChar == 'S') currentChar = 'a'; //Set the starting char to 'a' so it can do the diff in char instead of from 'S'
|
|
|
|
|
|
|
|
|
|
return adjCoords
|
|
|
|
|
.Where(coord => coord.X >= 0 && coord.X < MaxX)
|
|
|
|
|
.Where(coord => coord.Y >= 0 && coord.Y < MaxY)
|
|
|
|
|
.Where(coord => (_map[coord.X, coord.Y] != 'E' && (_map[coord.X, coord.Y] - currentChar) <= 1) //At most upper height is 1, can drop any amount
|
|
|
|
|
|| (_map[coord.X, coord.Y] == 'E' && (currentChar == 'z' || currentChar == 'y'))) //E's height IS 'z', so it can go z->z or y->z for the final move.
|
|
|
|
|
.ToList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AStarTraversal()
|
|
|
|
|
{
|
|
|
|
|
var activeCoords = new List<Coord>();
|
|
|
|
|
activeCoords.Add(_start);
|
|
|
|
|
var visitedCoords = new List<Coord>();
|
|
|
|
|
|
|
|
|
|
while (activeCoords.Any())
|
|
|
|
|
{
|
|
|
|
|
var checkCoord = activeCoords.OrderByDescending(x => x.CostDistance).Last();
|
|
|
|
|
|
|
|
|
|
if (checkCoord.X == _end.X && checkCoord.Y == _end.Y)
|
|
|
|
|
{
|
|
|
|
|
//We found the destination and we can be sure (Because the the OrderBy above)
|
|
|
|
|
//That it's the most low cost option.
|
|
|
|
|
var coord = checkCoord;
|
|
|
|
|
var steps = new List<string>();
|
|
|
|
|
while (true)
|
|
|
|
|
{
|
|
|
|
|
steps.Add($"({coord.X},{coord.Y}) -> {_map[coord.X, coord.Y]}");
|
|
|
|
|
coord = coord.Parent;
|
|
|
|
|
if (coord == null)
|
|
|
|
|
{
|
|
|
|
|
Steps = steps.Reverse<string>().ToArray();
|
|
|
|
|
TotalSteps = steps.Count-1; //N coords touched, so N-1 steps.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
visitedCoords.Add(checkCoord);
|
|
|
|
|
activeCoords.Remove(checkCoord);
|
|
|
|
|
|
|
|
|
|
var adjacentCoords = GetAdjacentCoords(checkCoord);
|
|
|
|
|
|
|
|
|
|
foreach (var adjCoor in adjacentCoords)
|
|
|
|
|
{
|
|
|
|
|
//Skip to next coord if we've already visited
|
|
|
|
|
if (visitedCoords.Any(x => x.X == adjCoor.X && x.Y == adjCoor.Y))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
//Its already pending as an active coord, but we need to re-check and update it if the cost is smaller
|
|
|
|
|
if (activeCoords.Any(x => x.X == adjCoor.X && x.Y == adjCoor.Y))
|
|
|
|
|
{
|
|
|
|
|
var existingCoord = activeCoords.First(x => x.X == adjCoor.X && x.Y == adjCoor.Y);
|
|
|
|
|
if (existingCoord.CostDistance > checkCoord.CostDistance)
|
|
|
|
|
{
|
|
|
|
|
activeCoords.Remove(existingCoord);
|
|
|
|
|
activeCoords.Add(adjCoor);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
//New coord so we are adding it to the list
|
|
|
|
|
activeCoords.Add(adjCoor);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// This is a brute force method, it works for the sample but not my input, it takes way to long as it tries to explore every single path and square.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="visited"></param>
|
|
|
|
|
/// <param name="pos"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
[Obsolete]
|
|
|
|
|
public List<Coord> Traverse(List<Coord> visited, Coord pos)
|
|
|
|
|
{
|
|
|
|
|
visited.Add(pos); //Signify that we visited this coord in our search
|
|
|
|
|
char val = _map[pos.X, pos.Y];
|
|
|
|
|
if (val == 'E')
|
|
|
|
|
{
|
|
|
|
|
//We only want to return if the last visited char is 'z' since it needs to complete alphabet before continuing
|
|
|
|
|
var lastVisitedLocation = visited[visited.Count - 2];
|
|
|
|
|
return _map[lastVisitedLocation.X, lastVisitedLocation.Y] == 'z' ? visited : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (val == 'S') val = 'a'; //So its on an even playing field.
|
|
|
|
|
|
|
|
|
|
List<Coord> shortestPath = null;
|
|
|
|
|
//left
|
|
|
|
|
var newCoord = new Coord(pos.X, pos.Y - 1);
|
|
|
|
|
if ((pos.Y - 1) >= 0
|
|
|
|
|
&& !visited.Any(c => c.X == newCoord.X && c.Y == newCoord.Y)
|
|
|
|
|
&& _map[newCoord.X, newCoord.Y] - val <= 1)
|
|
|
|
|
{
|
|
|
|
|
var newVisited = visited.Select(x => x).ToList();
|
|
|
|
|
var path = Traverse(newVisited, newCoord);
|
|
|
|
|
if (path != null && (shortestPath == null || path.Count() < shortestPath.Count()))
|
|
|
|
|
shortestPath = path;
|
|
|
|
|
}
|
|
|
|
|
//right
|
|
|
|
|
newCoord = new Coord(pos.X, pos.Y + 1);
|
|
|
|
|
if ((pos.Y + 1) < MaxY
|
|
|
|
|
&& !visited.Any(c => c.X == newCoord.X && c.Y == newCoord.Y)
|
|
|
|
|
&& _map[newCoord.X, newCoord.Y] - val <= 1)
|
|
|
|
|
{
|
|
|
|
|
var newVisited = visited.Select(x => x).ToList();
|
|
|
|
|
var path = Traverse(newVisited, newCoord);
|
|
|
|
|
if (path != null && (shortestPath == null || path.Count() < shortestPath.Count()))
|
|
|
|
|
shortestPath = path;
|
|
|
|
|
}
|
|
|
|
|
//up
|
|
|
|
|
newCoord = new Coord(pos.X - 1, pos.Y);
|
|
|
|
|
if ((pos.X - 1) >= 0
|
|
|
|
|
&& !visited.Any(c => c.X == newCoord.X && c.Y == newCoord.Y)
|
|
|
|
|
&& _map[newCoord.X, newCoord.Y] - val <= 1)
|
|
|
|
|
{
|
|
|
|
|
var newVisited = visited.Select(x => x).ToList();
|
|
|
|
|
var path = Traverse(newVisited, newCoord);
|
|
|
|
|
if (path != null && (shortestPath == null || path.Count() < shortestPath.Count()))
|
|
|
|
|
shortestPath = path;
|
|
|
|
|
}
|
|
|
|
|
//down
|
|
|
|
|
newCoord = new Coord(pos.X + 1, pos.Y);
|
|
|
|
|
if ((pos.X + 1) < MaxX
|
|
|
|
|
&& !visited.Any(c => c.X == newCoord.X && c.Y == newCoord.Y)
|
|
|
|
|
&& _map[newCoord.X, newCoord.Y] - val <= 1)
|
|
|
|
|
{
|
|
|
|
|
var newVisited = visited.Select(x => x).ToList();
|
|
|
|
|
var path = Traverse(newVisited, newCoord);
|
|
|
|
|
if (path != null && (shortestPath == null || path.Count() < shortestPath.Count()))
|
|
|
|
|
shortestPath = path;
|
|
|
|
|
}
|
|
|
|
|
return shortestPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class Coord
|
|
|
|
|
{
|
|
|
|
|
public Coord() { }
|
|
|
|
|
public Coord(int x, int y)
|
|
|
|
|
{
|
|
|
|
|
this.X = x;
|
|
|
|
|
this.Y = y;
|
|
|
|
|
}
|
|
|
|
|
public int X { get; set; }
|
|
|
|
|
public int Y { get; set; }
|
|
|
|
|
public int Cost { get; set; }
|
|
|
|
|
public int Distance { get; set; }
|
|
|
|
|
public int CostDistance { get { return Cost + Distance; } }
|
|
|
|
|
public Coord Parent { get; set; }
|
|
|
|
|
|
|
|
|
|
//An estimated distance to a specific target
|
|
|
|
|
public void SetDistance(Coord target)
|
|
|
|
|
{
|
|
|
|
|
this.Distance = Math.Abs(target.X - X) + Math.Abs(target.Y - Y);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|