Repository: networkx/networkx

Issue:
A* (A star) function astar_path returns a wrong answer
Suppose we have a graph below:
![astarexample](https://user-images.githubusercontent.com/51309106/58777405-b8cbd980-8600-11e9-9630-166c665db555.png)
Find shortest path from N5 to N0.
Numbers in bracket alongside each node are heuristic values estimating distance to n0. (This is a bad example of heuristic function I constructed to use in my classroom.)
The best path from n5 to n0 is:  
n5→n4→n3→n2→n1→n0
What networkx.algorithms.shortest_paths.astar.astar_path returns: 
[n5, n1, n0] 
which is apparently not correct.



Discussion:
Here's a similar very simple example that shows the same bug:

    Gedges = [(1,0,11), (1,2,1), (2,0,6)]
    G = nx.DiGraph()
    G.add_weighted_edges(Gedges)
    def heur(u,v):
        distance = {0:0, 1:36, 2:16}
        return distance[u]
    nx.astart_path(G,1,0,heur)    # returns  [1, 0]  instead of shorter [1,2,0]

Definitely a bug here. I think when the code finds the target it never checks whether the heuristic estimate of distance is correct... It just stops.  Why don't any tests find this bug? Am I missing something?  We need a PR with a test and some fixed code.
Hey,
in your example @dschult, I think the result is actually correct as you do not have an admissible heuristic (i.e. one that underestimates cost to the goal) and in that case A* is not guaranteed to find an optimal solution.

In the case of the main example, there actually seems to be a bug. As far as I can tell, the problem seems to be the checking of already explored nodes. It stores explored nodes in a dictionary, so when the same node is encountered twice, it does not explore the node again. In the example here, a "bad path" is found first (`n5`->`n1`->`n0`), which correctly gets put at the end of the priority queue (and is not yet fully explored!). The problem is though, that all other paths through `n1` do not get explored because `n1` in stored among the already explored nodes. Therefore, the "bad path" gets returned as solution.  

I'll test it a bit more and see if I can come up with a PR.

Resolving patch:
diff --git a/networkx/algorithms/shortest_paths/astar.py b/networkx/algorithms/shortest_paths/astar.py
--- a/networkx/algorithms/shortest_paths/astar.py
+++ b/networkx/algorithms/shortest_paths/astar.py
@@ -110,13 +110,18 @@ def heuristic(u, v):
             return path
 
         if curnode in explored:
-            continue
+            # Do not override the parent of starting node
+            if explored[curnode] is None:
+                continue
+
+            # Skip bad paths that were enqueued before finding a better one
+            qcost, h = enqueued[curnode]
+            if qcost < dist:
+                continue
 
         explored[curnode] = parent
 
         for neighbor, w in G[curnode].items():
-            if neighbor in explored:
-                continue
             ncost = dist + w.get(weight, 1)
             if neighbor in enqueued:
                 qcost, h = enqueued[neighbor]


Test patch:
diff --git a/networkx/algorithms/shortest_paths/tests/test_astar.py b/networkx/algorithms/shortest_paths/tests/test_astar.py
--- a/networkx/algorithms/shortest_paths/tests/test_astar.py
+++ b/networkx/algorithms/shortest_paths/tests/test_astar.py
@@ -1,9 +1,8 @@
-from nose.tools import assert_equal
+from nose.tools import assert_equal, assert_in
 from nose.tools import assert_raises
 from nose.tools import raises
 
 from math import sqrt
-from random import random, choice
 
 import networkx as nx
 from networkx.utils import pairwise
@@ -23,27 +22,24 @@ def setUp(self):
         self.XG = nx.DiGraph()
         self.XG.add_weighted_edges_from(edges)
 
-    def test_random_graph(self):
-        """Tests that the A* shortest path agrees with Dijkstra's
-        shortest path for a random graph.
+    def test_multiple_optimal_paths(self):
+        """Tests that A* algorithm finds any of multiple optimal paths"""
+        heuristic_values = {"a": 1.35, "b": 1.18, "c": 0.67, "d": 0}
 
-        """
-
-        G = nx.Graph()
+        def h(u, v):
+            return heuristic_values[u]
 
-        points = [(random(), random()) for _ in range(100)]
+        graph = nx.Graph()
+        points = ["a", "b", "c", "d"]
+        edges = [("a", "b", 0.18), ("a", "c", 0.68),
+                 ("b", "c", 0.50), ("c", "d", 0.67)]
 
-        # Build a path from points[0] to points[-1] to be sure it exists
-        for p1, p2 in pairwise(points):
-            G.add_edge(p1, p2, weight=dist(p1, p2))
+        graph.add_nodes_from(points)
+        graph.add_weighted_edges_from(edges)
 
-        # Add other random edges
-        for _ in range(100):
-            p1, p2 = choice(points), choice(points)
-            G.add_edge(p1, p2, weight=dist(p1, p2))
-
-        path = nx.astar_path(G, points[0], points[-1], dist)
-        assert_equal(path, nx.dijkstra_path(G, points[0], points[-1]))
+        path1 = ["a", "c", "d"]
+        path2 = ["a", "b", "c", "d"]
+        assert_in(nx.astar_path(graph, "a", "d", h), (path1, path2))
 
     def test_astar_directed(self):
         assert_equal(nx.astar_path(self.XG, 's', 'v'), ['s', 'x', 'u', 'v'])
@@ -87,6 +83,32 @@ def test_astar_undirected3(self):
         assert_equal(nx.astar_path(XG4, 0, 2), [0, 1, 2])
         assert_equal(nx.astar_path_length(XG4, 0, 2), 4)
 
+    """ Tests that A* finds correct path when multiple paths exist
+        and the best one is not expanded first (GH issue #3464)
+    """
+    def test_astar_directed3(self):
+        heuristic_values = {"n5": 36, "n2": 4, "n1": 0, "n0": 0}
+
+        def h(u, v):
+            return heuristic_values[u]
+
+        edges = [("n5", "n1", 11), ("n5", "n2", 9),
+                 ("n2", "n1", 1), ("n1", "n0", 32)]
+        graph = nx.DiGraph()
+        graph.add_weighted_edges_from(edges)
+        answer = ["n5", "n2", "n1", "n0"]
+        assert_equal(nx.astar_path(graph, "n5", "n0", h), answer)
+
+    """ Tests that that parent is not wrongly overridden when a
+        node is re-explored multiple times.
+    """
+    def test_astar_directed4(self):
+        edges = [("a", "b", 1), ("a", "c", 1), ("b", "d", 2),
+                 ("c", "d", 1), ("d", "e", 1)]
+        graph = nx.DiGraph()
+        graph.add_weighted_edges_from(edges)
+        assert_equal(nx.astar_path(graph, "a", "e"), ["a", "c", "d", "e"])
+
 # >>> MXG4=NX.MultiGraph(XG4)
 # >>> MXG4.add_edge(0,1,3)
 # >>> NX.dijkstra_path(MXG4,0,2)