/// <summary> /// Dijkstra's algorithm, shortest paths between nodes in a graph /// https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm /// Computes the distance table between two nodes in a graph /// All positions have 8 degrees of freedom, and the cost of moving between positions are equal to /// 1 + the absolute difference in value. /// </summary> /// <param name="items">2D value array</param> /// <param name="initial">Start position</param> /// <param name="end">End positions</param> public static int[,] Dijkstra(IMoveCost[,] items, Position initial, Position end) { // Let the node at which we are starting be called the initial node. Let the distance of node Y be the distance from the initial node to Y. // Dijkstra's algorithm will assign some initial distance values and will try to improve them step by step. int W = items.GetLength(0); int H = items.GetLength(1); int[,] distance = new int[W, H]; bool[,] visited = new bool[W, H]; // 1. Assign to every node a tentative distance value: set it to zero for our initial node and to infinity for all other nodes. // 2. Set the initial node as current. Mark all other nodes unvisited. Create a set of all the unvisited nodes called the unvisited set. List<Position> unvisited = new List<Position>(); for (int x = 0; x < W; x++) for (int y = 0; y < H; y++) { distance[x, y] = int.MaxValue; visited[x, y] = false; if (!(initial.x == x && initial.y == y)) unvisited.Add(new Position(x,y)); } distance[initial.x, initial.y] = 0; Position current = initial; while (true) { // For the current node, consider all of its unvisited neighbors and calculate their tentative distances. // Compare the newly calculated tentative distance to the current assigned value and assign the smaller one. // For example, if the current node A is marked with a distance of 6, and the edge connecting it with a neighbor B // has length 2, then the distance to B (through A) will be 6 + 2 = 8. If B was previously marked with a distance // greater than 8 then change it to 8. Otherwise, keep the current value. var neighbors = GetUnvisitedNeighbors(visited, current); foreach (Position neighbor in neighbors) { // get the cost of moving to the neighbor int cost = items[neighbor.x, neighbor.y].MoveCost(items[current.x, current.y]); // if the path is shorter than a previous path to the neighbor, update it int path = MOVE_PENALTY + cost + distance[current.x, current.y]; if (path < distance[neighbor.x, neighbor.y]) distance[neighbor.x, neighbor.y] = path; } // When we are done considering all of the neighbors of the current node, mark the current node as visited // and remove it from the unvisited set. A visited node will never be checked again. unvisited.Remove(current); visited[current.x, current.y] = true; // If the destination node has been marked visited (when planning a route between two specific nodes) // or if the smallest tentative distance among the nodes in the unvisited set is infinity // (when planning a complete traversal; occurs when there is no connection between the initial node and // remaining unvisited nodes), then stop. The algorithm has finished. if (visited[end.x, end.y]) break; // Otherwise, select the unvisited node that is marked with the smallest tentative distance, // set it as the new "current node", and go back to step 3. current = (from p in unvisited orderby distance[p.x, p.y] ascending select new Position(p.x, p.y)).First(); } return distance; }
/// <summary> /// Return a list of all the unvisited positions around the given position /// </summary> /// <param name="visited">An bool array of visited positions</param> /// <param name="current">Position to get neighbors of</param> /// <returns></returns> internal static List<Position> GetUnvisitedNeighbors(bool[,] visited, Position current) { // get a list of all neighbors var list = GetNeighbors(current, visited.GetLength(0), visited.GetLength(1)); // return the list without all visited neighbors return list.Where(p => !visited[p.x, p.y]).ToList(); }
/// <summary> /// Computes the shortest path between two positions, in a 2D distance table. /// All positions have 8 degrees of freedom /// </summary> /// <param name="values">2D Integer distance array</param> /// <param name="initial">Start position</param> /// <param name="end">End positions</param> /// <returns></returns> public static List<Position> GetShortestPathFromDistance(int[,] dist, Position initial, Position end) { // width and height of the given array int width = dist.GetLength(0); int height = dist.GetLength(1); // We have to make sure we don't backtrack when moving between neighbors. // This can be ignored if we make the assumption of movement cost to be at least 1. bool[,] visited = new bool[width, height]; for (int x = 0; x < width; x++) for (int y = 0; y < height; y++) visited[x, y] = false; List<Position> path = new List<Position>(); // We start at the end Position current = end; // while we are no in the initial position, while (!(current.x == initial.x && current.y == initial.y)) { // add the position to the list path.Add(current); // set the position as visited visited[current.x, current.y] = true; // find all unvisited neighbors var list = GetUnvisitedNeighbors(visited, current); // the next position is the neighbor with the lowest value current = (from item in list orderby dist[item.x, item.y] ascending select new Position(item.x, item.y)).First(); } path.Add(current); path.Reverse(); return path; }
/// <summary> /// Return a list of all the positions around the given position /// </summary> /// <param name="visited">An bool array of visited positions</param> /// <param name="current">Position to get neighbors of</param> /// <returns></returns> internal static List<Position> GetNeighbors(Position current, int width, int height) { // list of neighbors List<Position> neighbors = new List<Position>(); // loop over a 3x3 square with the current position in the middle for (int x = current.x - 1; x <= current.x + 1; x++) { for (int y = current.y - 1; y <= current.y + 1; y++) { // skip position if it's an edge or middle position if (x < 0 || x >= width || y < 0 || y >= height || (current.x == x && current.y == y)) continue; neighbors.Add(new Position(x, y)); } } return neighbors; }
/// <summary> /// Computes the shortest path between two positions, in a 2D table. /// All positions have 8 degrees of freedom, and the cost of moving between positions are equal to /// 1 + the absolute difference in value. /// </summary> /// <param name="values">2D Cell array</param> /// <param name="initial">Start position</param> /// <param name="end">End positions</param> /// <returns></returns> public static List<Position> GetShortestPath(IMoveCost[,] values, Position initial, Position end) { // Use Dijkstra's algorithm to compute the distance table for the given values int[,] dist = Dijkstra(values, initial, end); // traverse the shortest path and return list return GetShortestPathFromDistance(dist, initial, end); }