The Optimal Path problem

One of the major problems in logistics is the optimal path problem. Imagine you are a delivery driver and have been tasked to deliver packages in twenty-five different places in New York City. How do you calculate the optimum route that you should drive so that your time taken is minimum? Ignoring external factors like traffic jams, and considering the road speeds, you will try to optimize so that most of travel covers wider avenues than local streets. But that is just one part of the problem.

This problem is really two problems disguised in one.

  1. Pathfinding. How far is it from stop A to stop B? That is a shortest-path question on the road network.
  2. Ordering. Given the distance between every pair of stops, in which order should I visit them? That is the travelling salesman problem (TSP).

Today in this blog, we will go through the first part of the problem aka. Pathfinding problem. We will go over four algorithms most widely used across the industry. Before we delve deep into it, let’s see how the algorithms affect optimal time.

The Algorithms

In this image, we are graphing the four algorithms we will discuss. Imagine a city with avenues, streets and areas with more crowds. A river flows through it only allowing traffic over some bridges. We have points associated with each of them based on speed and traffic.

Each algorithm tried a different approach to arrive at a route. Only two of them are optimal. So, how are these calculating the routes?

The first graph is Breadth First Search (BFS). In this case it ignores the costs (or weights) assigned to each road, and tries to always calculate the shortest route (least number of edges). So, in this case, it spent a lot of time on the streets, taking it’s score up – thus was not optimal cost (but achieved least hops).

The second graph is Dijkstra. This considered the weights of the roads and optimized the route based on total score. Even though it traversed a lot of the graph, it still came up with the most optimal routing.

The third is A*. This keeps all the benefits of the above, but uses heuristics to traverse less path. The only issue with this algorithm is that if we do not have a proper heuristic function, the route will not be optimal.

The fourth one is greedy and uses heuristics to force towards the destination. It may not always be optimal. As you can see in the diagram, it went straight towards the destination, hit the river and had to step back towards the bridge. This increased the score quite a bit.

The image also contains a benchmark comparison between the four algorithms. Take a look at the metrics (Cost: Sum of all Weights, Expanded: How many cells were opened, ms: milliseconds).

The Problem

We will make one more stop before going to the details. Let’s first define our own city with some houses. Each house leads to one or more other houses. Based on where you are coming from you have to pay toll when you go to the house. Our target will be to go to the destination with the minimum toll paid. The coordinates are needed to calculate the distance between the houses.

There are multiple problems finding a path here as the costs are not optimized. To go from A -> G, the cheapest path is not the shortest. Direct traversal to B and following the path is more expensive than taking a long way through C. Same happens from B to G. The longest route is the smallest cost. The ideal path is:

A -> C -> B -> E -> F -> G (cost: 7)

We will build our algorithms based on the image above.

Breadth First Search (BFS)

BFS explores a graph one layer at a time: first the start node’s neighbors, then nodes two edges away, and so on. It finds a path with the fewest edges in an unweighted graph – or when every edge has the same cost.

BFS does not respect any cost, however it will calculate the least number of steps. So if I want to move from house A to house G, BFS will find the shortest path. It is used when you do not bother about weights, or all weights are the same. For our house example above, let’s write the step-by-step frontier expansion using BFS.

What does step-by-step frontier expansion table depict? The frontier is the set of discovered-but-unexplored nodes, typically held in a queue or priority queue. Expanding a node means pulling it from the frontier and examining its neighbors, adding any new ones to the frontier. The order of expansion is what defines each algorithm — BFS explores oldest-first, Dijkstra expands the cheapest node, A* expands the most promising, and greedy rushes toward the goal. In short, expansion is the engine that drives the search forward toward the goal.
StepExpandedFrontierVisited
1AB, CA, B, C
2BC, D, EA, B, C, D, E
3CD, EA, B, C, D, E
4DE, GA, B, C, D, E, G
5EG, F, HA, B, C, D, E, F, G, H
6GF, HA, B, C, D, E, F, G, H
BFS
BFS path: ['A', 'B', 'D', 'G'] cost: 10, hops: 3 (optimized)

Use Cases

Some sample use cases for this algorithm,

  • Finding the closeness of two people in a social network (get hop count).
  • Find minimum number of moves between two points in a simple puzzle.
  • Discovering every point in a puzzle that is lower than a certain number of steps.
  • Explore any unweighted graphs.

We should not use BFS when edges have different costs, for example when roads have tolls, differ in distance or speed etc.

Dijkstra

Dijkstra’s algorithm tracks the total cost of reaching each node and explores the least-cost option next. It finds shortest paths when edge costs are non-negative. It’s a strong choice when costs vary and you don’t have a useful estimate of the distance to the destination.

Dijkstra’s algorithm repeatedly selects the unvisited node with the smallest known cost, then relaxes its outgoing edges: it updates a neighbor’s distance if going through that node gives a cheaper path.

Let’s check the logical step-by-step frontier expansion,

StepExpandedgFrontier node(g)Expanded order
1A0C(1), B(3)A
2C1B(2)A, C
3B2D(4), E(4)A, C, B
4D4E(4), G(9)A, C, B, D
5E4F(6), G(8), H(8)A, C, B, D, E
6F6G(7), H(8)A, C, B, D, E, F
7G7H(8)A, C, B, D, E, F, G
Dijkstra
Dijkstra path: ['A', 'C', 'B', 'E', 'F', 'G'] cost: 7
We used a term called Relax. In graph theory, the Edge Relaxation property is defined as updating a path to the least cost when there are multiple paths to the same node. For example, in our example above, for A -> B, it is better to traverse A -> C -> B. In this case, the path will be updated to later.

Use Cases

  • Finding the lowest cost path through a weighted network.
  • Calculating the shortest path from one starting point to many destinations.
  • Routing when links have different costs, like latency or distance.

This algorithm cannot be used when an edge has negative cost. This does iterate a lot of area around the path, so it may feel slower. Most cases will work better with A*.

A* search

A* combines current cost and using heuristics estimates remaining cost. It uses both the route so far and a prediction of what remains. With non-negative edge costs and an appropriate heuristic, A* can find an optimal path; however it relies on the heuristic to not overestimate cost. Normally we use Euclidean distance or Manhattan distance to calculate the remaining path. We will use Manhattan distance because it is easier to calculate manually if you want to wrap your head around the expansion table. For cities, most of the time Manhattan distance is the one that is preferred as they have a grid-like layout of avenues and streets.

Euclidean distance represents the shortest straight-line distance between two points in Euclidean space. It is the easiest to understand and most used metric to calculate distance. Distance is calculated as follows:

d = √((x2 - x1)2 + (y2 - y1)2)

Manhattan distance, also called Taxicab or City Block distance, calculates the sum of the absolute differences of Cartesian coordinates.

d = |x2 - x1| + |y2 - y1|

We normally use Manhattan distance for A* algorithms because it is better suited for grid-like paths that are restricted to mostly horizontal and vertical paths. Since our example has quite a few diagonals, Euclidean distance is a better fit. Manhattan distance is admissible, but on graphs with shortcut edges (where an edge weight is smaller than the implied distance), it is inconsistent. Closed-set A* with an inconsistent heuristic can return a suboptimal path — here it returns cost 8 instead of 7. Using re-expansion restores the optimality guarantee.

To be guaranteed optimal, heuristic for A* should be:

1. Admissible - never overestimates
2. Consistent — satisfies the triangle inequality along edges

For our graph, Manhattan is admissible but NOT consistent.

Here is the frontier expansion table when we used the Manhattan distance,

StepExpandedghfFrontier node(f)Closed order
1A044B(5), C(5)A
2B325C(5), D(7), E(7)A, B
3C145D(7), E(7)A, B, C
4D527E(7), G(10)A, B, C, D
5E527F(9), G(9), H(13)A, B, C, D, E
6F729G(8), H(13)A, B, C, D, E, F
7G808H(13)A, B, C, D, E, F, G
A* (closed set)
A* path (closed, Manhattan): ['A', 'B', 'E', 'F', 'G'] cost: 8

Re-expansion A*

The problem with A* is that it is normally closed order and only returns the optimal path when the heuristic is proper. We can modify the algorithm to look back nodes and reopen if a better path is found. There is no universally accepted name for this, but one common name is called A* with re-expansion. This maintains the g value and can reopen nodes when cost decreases. If we create this, the frontier expansion changes,

StepExpandedghfFrontier node(f)Expanded order
1A044B(5), C(5)A
2C145B(4)A, C
3B224D(6), E(6)A, C, B
4D426E(6), G(9)A, C, B, D
5E426F(8), G(8), H(12)A, C, B, D, E
6F628G(7), H(12)A, C, B, D, E, F
7G707H(12)A, C, B, D, E, F, G
Lifelong Planning A*
A* path (optimized, Manhattan): ['A', 'C', 'B', 'E', 'F', 'G'] cost: 7

Use Cases

  • Finding a route to a specific destination on a map.
  • Pathfinding in games or robotics when movement costs vary.
  • Search problems where a useful heuristic can guide exploration without sacrificing the desired optimalityguarantee.

A* should not be used if a suitable heuristic is not available. It will not provide optimal results.

Greedy Best-First Search

Greedy Best-First Search chooses the node that appears closest to the goal according to a heuristic. Unlike A* it ignores the cost that has already been paid to reach the node. This makes it very fast, but the route may not be optimal.

We give below the frontier expansion table,

StepExpandedhFrontier node(h)Expanded order
1A4B(2), C(4)A
2B2D(2), E(2), C(4)A, B
3D2G(0), E(2), C(4)A, B, D
4G0E(2), C(4)A, B, D, G
Greedy Best-First Search
Greedy path: ['A', 'B', 'D', 'G'] cost: 10

Use Cases

  • Finding a possible route quickly when an exact optimum is not essential.
  • Search tasks where a useful heuristic points strongly toward the goal.
  • Applications where a fast, “good enough” answer is more valuable than proving it is best.

Quick Decision Guide

  1. Edges have no cost/ costs the same: BFS
  2. Costs vary, and do not have a proper heuristic that can be used: Dijkstra
  3. Speed matters more than the cheapest route: Greedy Best-First
  4. Cheapest path to a goal, and you have a heuristic function: A*

Time/ Space Complexity

AlgorithmTime ComplexitySpace Complexity
Breadth-First SearchO(V + E)O(V)
Dijkstra’s AlgorithmO(V + E) log V)O(V)
A* SearchO (worst: O(bd)O(V)
Greedy Best-First SearchO(V + E)O(V)
Performance
  • V: Number of Vertices
  • E: Number of Edges
  • b: Branching factor
  • d: Depth of the shallowest goal

Implementations

Any tech blog is incomplete without implementations. Even though I normally start with implementations, in this case, I deferred it to last. With the advent of fine AI models, manual coding has taken a step back. We can still build beautiful programs without understanding anything remotely about coding.

But here it is, Python-based code implementations for these. And I will show you couple of methods for A* and describe each one.

Let’s define two structures to keep the path and coordinates.

def __init__(self):
  """Set up the sample graph (path_graph) and the node coordinates 
  used by the heuristic."""
  # Adjacency list: node -> list of (neighbor, edge weight)
  self.path_graph = {
    "A": [("B", 3), ("C", 1)],
    "B": [("D", 2), ("E", 2)],
    "C": [("B", 1)],
    "D": [("G", 5)],
    "E": [("F", 2), ("G", 4), ("H", 4)],
    "F": [("G", 1), ("H", 5)],
    "G": [],
    "H": []
  }

  # Node -> (x, y) position, used by A* and greedy to estimate 
  # distance to the goal
  self.coords = {
    "A": (0,0),
    "B": (1,1),
    "C": (0,2),
    "D": (2,0),
    "E": (2,2),
    "F": (3,3),
    "G": (3,1),
    "H": (1,3)
  }

BFS

def bfs_path(self, start, goal, debug=False):
        """
        Breadth-first search over self.path_graph, ignoring edge weights.

        Finds the path with the fewest edges (not necessarily the lowest weight).

        Args:
            start: node to begin at.
            goal: node to reach.
            debug: if True, print a step-by-step table of the queue after each expansion.

        Returns:
            (path, cost): path is the list of nodes from start to goal and cost is the
            number of edges in it. Returns (None, inf) if goal is unreachable.
        """
        queue = deque([start])
        visited = {start}
        parent = {start: None}
        rows = []  # debug table rows

        while queue:
            node = queue.popleft()
            if node == goal:
                if debug:
                    rows.append([len(rows) + 1, node, self._fmt_frontier(queue), ", ".join(sorted(visited))])
                break

            for nbr, _ in self.path_graph.get(node, []):
                if nbr not in visited:
                    visited.add(nbr)
                    parent[nbr] = node
                    queue.append(nbr)

            if debug:
                rows.append([len(rows) + 1, node, self._fmt_frontier(queue), ", ".join(sorted(visited))])

        if debug:
            self._print_table(f"BFS {start} -> {goal}", ["Step", "Expanded", "Frontier (queue)", "Visited"], rows)

        if goal not in parent:
            return None, float("inf")

        # Reconstruct path
        path = []
        cur = goal
        while cur is not None:
            path.append(cur)
            cur = parent[cur]
        path.reverse()

        cost = len(path) - 1  # number of edges
        return path, cost
  • Uses a queue (FIFO) to process nodes in order of discovery.
  • Marks each node as visited the first time it’s seen — never revisited.
  • Records a “parent” for every new node so the path can be traced back.
  • Once the goal is found, walk backward through parent pointers.
  • Reverse the result to produce the final start → goal path.

Dijkstra

def dijkstra_path(self, start, goal, debug=False):
        """
        Dijkstra's shortest-path search over self.path_graph.

        Always expands the unvisited node with the lowest known distance from start,
        so it finds the minimum total weight path (weights must be non-negative).

        Args:
            start: node to begin at.
            goal: node to reach.
            debug: if True, print a step-by-step table of the frontier after each expansion.

        Returns:
            (path, cost): path is the list of nodes from start to goal and cost is the
            minimum total edge weight. Returns (None, inf) if goal is unreachable.
        """
        pq = [(0, start)]
        dist = {start: 0}
        parent = {start: None}
        visited = set()
        order = []  # expansion order, for the debug table
        rows = []   # debug table rows

        while pq:
            cur_dist, u = heapq.heappop(pq)
            if u in visited:
                continue
            visited.add(u)
            order.append(u)

            if u == goal:
                if debug:
                    frontier = sorted(((v, d) for d, v in pq if v not in visited and d == dist[v]), key=lambda e: (e[1], e[0]))
                    rows.append([len(rows) + 1, u, cur_dist, self._fmt_frontier(frontier), ", ".join(order)])
                break

            for v, w in self.path_graph.get(u, []):
                if v in visited:
                    continue
                new_dist = cur_dist + w
                if new_dist < dist.get(v, float("inf")):
                    dist[v] = new_dist
                    parent[v] = u
                    heapq.heappush(pq, (new_dist, v))

            if debug:
                # Only live entries: not yet expanded and matching the best known distance
                frontier = sorted(((v, d) for d, v in pq if v not in visited and d == dist[v]), key=lambda e: (e[1], e[0]))
                rows.append([len(rows) + 1, u, cur_dist, self._fmt_frontier(frontier), ", ".join(order)])

        if debug:
            self._print_table(f"Dijkstra {start} -> {goal}",
                              ["Step", "Expanded", "g", "Frontier node(g)", "Expanded order"], rows)

        if goal not in parent:
            return None, float("inf")

        # Reconstruct path
        path = []
        cur = goal
        while cur is not None:
            path.append(cur)
            cur = parent[cur]
        path.reverse()

        return path, dist[goal]
  • Greedily expands the unvisited node with the lowest known distance from the start.
  • This “cheapest-so-far first” strategy guarantees the shortest total cost.
  • Uses a priority queue (min-heap) to always pick the nearest unexplored node.
  • For each neighbor, checks if a cheaper route was found — if so, updates distance and re-prioritizes.
  • When goal isfound, walk backward from the goal through parent pointers to reconstruct the path.
  • Return the path and its total cost (dist[goal]).

A* (closed)

def astar_closed_path(self, start, goal, use_manhattan, debug=False):
        """
        A* search over self.path_graph using self.coords for the heuristic, with a closed set.

        Expands the node with the lowest f(n) = g(n) + h(n), where g is the cost so far
        and h is the Manhattan or Euclidean distance to goal. Once a node is expanded it is
        closed and never revisited, so the path is only guaranteed to be minimum-weight if
        the heuristic is consistent. With an inconsistent heuristic (e.g. Manhattan on this
        graph) it can return a costlier path; see astar_path for the version that fixes this.

        Args:
            start: node to begin at.
            goal: node to reach.
            use_manhattan: if True use the Manhattan heuristic, otherwise Euclidean.
            debug: if True, print a step-by-step table of the frontier after each expansion.

        Returns:
            (path, cost): path is the list of nodes from start to goal and cost is its
            total edge weight. Returns (None, inf) if goal is unreachable.
        """
        pq = []
        heapq.heappush(pq, (0, start))  # (f, node)

        g = {start: 0}         # best cost from start to node
        parent = {start: None}
        closed = set()
        order = []  # expansion order, for the debug table
        rows = []   # debug table rows

        def log_step(u):
            # Only live entries: node not closed and f matching its current g + h
            frontier = sorted(((v, f) for f, v in pq
                               if v not in closed and f == g[v] + self._h(v, goal, use_manhattan)),
                              key=lambda e: (e[1], e[0]))
            h = self._h(u, goal, use_manhattan)
            rows.append([len(rows) + 1, u, g[u], h, g[u] + h,
                         self._fmt_frontier(frontier), ", ".join(order)])

        while pq:
            _, u = heapq.heappop(pq)

            if u in closed:
                continue
            closed.add(u)
            order.append(u)

            if u == goal:
                if debug:
                    log_step(u)
                break

            for v, w in self.path_graph.get(u, []):
                if v in closed:
                    continue
                tentative_g = g[u] + w
                if tentative_g < g.get(v, float("inf")):
                    g[v] = tentative_g
                    parent[v] = u
                    if use_manhattan:
                        f = tentative_g + self.manhattan_heuristic(self.coords[v], self.coords[goal])
                    else:
                        f = tentative_g + self.euclidean_heuristic(self.coords[v], self.coords[goal])
                    heapq.heappush(pq, (f, v))

            if debug:
                log_step(u)

        if debug:
            self._print_table(f"A* (closed set) {start} -> {goal}, {'Manhattan' if use_manhattan else 'Euclidean'}",
                              ["Step", "Expanded", "g", "h", "f", "Frontier node(f)", "Closed order"], rows)

        if goal not in parent:
            return None, float("inf")

        # Reconstruct path
        path = []
        cur = goal
        while cur is not None:
            path.append(cur)
            cur = parent[cur]
        path.reverse()

        return path, g[goal]
  • Expands the node with the lowest f(n) = g(n) + h(n):
    • g(n) = actual cost so far from start.
    • h(n) = estimated remaining cost (Manhattan or Euclidean distance to the goal).
  • Uses a priority queue ordered by f(n), always expanding the most promising node first.
  • Tracks g (cost so far), parent (for path reconstruction), and a closed set (already-expanded nodes).
  • For each neighbor, checks if a cheaper route was found — if so, updates cost and re-prioritizes.
  • When goal is found, walk backward from the goal through parent pointers to reconstruct the path.
  • Return the path and its total edge cost (g[goal]).

Re-expansion A*

def astar_reexpansion_path(self, start, goal, use_manhattan, debug=False):
        """
        A* search over self.path_graph using self.coords for the heuristic.

        Expands the node with the lowest f(n) = g(n) + h(n), where g is the cost so far
        and h is the Manhattan or Euclidean distance to goal. Nodes are re-expanded when a
        cheaper path to them is found, so it finds the minimum-weight path as long as the
        heuristic never overestimates the true remaining cost.

        Args:
            start: node to begin at.
            goal: node to reach.
            use_manhattan: if True use the Manhattan heuristic, otherwise Euclidean.
            debug: if True, print a step-by-step table of the frontier after each expansion.

        Returns:
            (path, cost): path is the list of nodes from start to goal and cost is its
            total edge weight. Returns (None, inf) if goal is unreachable.
        """
        pq = []
        heapq.heappush(pq, (0, 0, start))  # (f, g, node)

        g = {start: 0}         # best cost from start to node
        parent = {start: None}
        order = []  # expansion order (a node appears twice if it was re-expanded), for debug
        rows = []   # debug table rows

        def log_step(u):
            # Only live entries: g matches the best known g for that node
            frontier = sorted(((v, f) for f, g_v, v in pq if g_v == g[v]),
                              key=lambda e: (e[1], e[0]))
            h = self._h(u, goal, use_manhattan)
            rows.append([len(rows) + 1, u, g[u], h, g[u] + h,
                         self._fmt_frontier(frontier), ", ".join(order)])

        while pq:
            _, g_u, u = heapq.heappop(pq)

            # Skip stale entries; nodes are re-expanded if a cheaper path is found later,
            # which keeps A* correct even when the heuristic is inconsistent.
            if g_u > g[u]:
                continue
            order.append(u)

            if u == goal:
                if debug:
                    log_step(u)
                break

            for v, w in self.path_graph.get(u, []):
                tentative_g = g[u] + w
                if tentative_g < g.get(v, float("inf")):
                    g[v] = tentative_g
                    parent[v] = u
                    if use_manhattan:
                        f = tentative_g + self.manhattan_heuristic(self.coords[v], self.coords[goal])
                    else:
                        f = tentative_g + self.euclidean_heuristic(self.coords[v], self.coords[goal])
                    heapq.heappush(pq, (f, tentative_g, v))

            if debug:
                log_step(u)

        if debug:
            self._print_table(f"A* {start} -> {goal}, {'Manhattan' if use_manhattan else 'Euclidean'}",
                              ["Step", "Expanded", "g", "h", "f", "Frontier node(f)", "Expanded order"], rows)

        if goal not in parent:
            return None, float("inf")

        # Reconstruct path
        path = []
        cur = goal
        while cur is not None:
            path.append(cur)
            cur = parent[cur]
        path.reverse()

        return path, g[goal]

This is best of A* and Dijkstra, difference is,

  • For each neighbor, checks if a cheaper route was found — if so, updates cost and re-prioritizes.
  • No closed set: stale entries in the queue are skipped by comparing the popped g against the best known g.

Greedy Best-First Search

def greedy_path(self, start, goal, use_manhattan, debug=False):
        """
        Greedy best-first search over self.path_graph.

        Uses f(n) = h(n) only: always expands the node that looks closest to goal by the
        Manhattan or Euclidean heuristic, ignoring the cost so far. Fast, but the path
        found is not guaranteed to be the cheapest.

        Args:
            start: node to begin at.
            goal: node to reach.
            use_manhattan: if True use the Manhattan heuristic, otherwise Euclidean.
            debug: if True, print a step-by-step table of the frontier after each expansion.

        Returns:
            (path, cost): path is the list of nodes from start to goal and cost is the
            actual total edge weight along that path. Returns (None, inf) if goal is
            unreachable.
        """
        pq = []
        if use_manhattan:
            heapq.heappush(pq, (self.manhattan_heuristic(self.coords[start], self.coords[goal]), start))
        else:
            heapq.heappush(pq, (self.euclidean_heuristic(self.coords[start], self.coords[goal]), start))

        parent = {start: None}
        visited = set()
        order = []  # expansion order, for the debug table
        rows = []   # debug table rows

        def log_step(u):
            # Distinct nodes still waiting, lowest h first
            frontier = sorted({(v, h) for h, v in pq if v not in visited}, key=lambda e: (e[1], e[0]))
            rows.append([len(rows) + 1, u, self._h(u, goal, use_manhattan),
                         self._fmt_frontier(frontier), ", ".join(order)])

        # We'll compute cost by summing edges along the reconstructed path
        # (Alternatively, track g[] as well, but GBFS doesn't use it for priorities.)
        # For simplicity we reconstruct first then sum.
        while pq:
            _, u = heapq.heappop(pq)
            if u in visited:
                continue
            visited.add(u)
            order.append(u)

            if u == goal:
                if debug:
                    log_step(u)
                break

            for v, w in self.path_graph.get(u, []):
                if v in visited:
                    continue
                # If we reached v via u first, set parent.
                # (We don't do "relaxation" based on g because GBFS doesn't optimize g.)
                if v not in parent:
                    parent[v] = u

                if use_manhattan:
                    heapq.heappush(pq, (self.manhattan_heuristic(self.coords[v], self.coords[goal]), v))
                else:
                    heapq.heappush(pq, (self.euclidean_heuristic(self.coords[v], self.coords[goal]), v))

            if debug:
                log_step(u)

        if debug:
            self._print_table(f"Greedy {start} -> {goal}, {'Manhattan' if use_manhattan else 'Euclidean'}",
                              ["Step", "Expanded", "h", "Frontier node(h)", "Expanded order"], rows)

        if goal not in parent:
            return None, float("inf")

        # Reconstruct path
        path = []
        cur = goal
        while cur is not None:
            path.append(cur)
            cur = parent[cur]
        path.reverse()

        # Sum actual weights along path
        # Build a quick lookup of weights from u to v
        weight_lookup = {}
        for u in self.path_graph:
            for v, w in self.path_graph[u]:
                weight_lookup[(u, v)] = w

        total_cost = 0
        for i in range(len(path) - 1):
            total_cost += weight_lookup[(path[i], path[i+1])]

        return path, total_cost
  • Expands the node that looks closest to the goal.
  • Uses a priority queue ordered by h(n), always expanding the most goal-like node first.
  • Tracks parent (for path reconstruction) and a visited set (to avoid revisiting).
  • For each neighbor, assigns a parent the first time it’s reached — no cost-based relaxation (it doesn’t track g).
  • When destination node is found, walk backward from the goal through parent pointers to reconstruct the path.
  • Sum the actual edge weights along the path to report the true cost.

Summary

In this issue, we discussed the first part of finding best navigation route for a delivery truck. In the next one we will discuss some more algorithms that expand on this knowledge and add ordering to multiple such stops through a destination (traveling salesman problem). Till then, happy coding.

Leave a Reply

Your email address will not be published. Required fields are marked *