6

I am currently working on an implementation of the A* Algorithm with irregular distances between two nodes. The graph containing the nodes is a directed and weighted graph. Every node is connected to at least one other node, there may also be symmetrical connections with different distances. A node is nothing more than a label and doesn't contain any special information

What I need is a heuristic to determine the shortest path from any node A to another node B as accurate as possible. I tried to use a heuristic that returns the distance to the nearest neighbor of a node, but of course that wasn't as effective as no heuristic at all (= Dijkstra).


My implementation of the A* Algorithm consists mainly of 2 classes, the class for the algorithm itself (AStar) and one for the nodes (Node). The code is heavily based on the Wikipedia pseudocode.

Source code of AStar.java

public class AStar {
    private AStar() {}

    private static Node[] reconstructPath(Map<Node, Node> paths, Node current) {
        List<Node> path = new ArrayList<Node>();
        path.add(0, current);
        while (paths.containsKey(current)) {
            current = paths.get(current);
            path.add(0, current);
        }
        return path.toArray(new Node[0]);
    }

    public static Node[] calculate(Node start, Node target, IHeuristic heuristic) {
        List<Node> closed = new ArrayList<Node>();
        PriorityQueue<Node> open = new PriorityQueue<Node>();
        Map<Node, Double> g_score = new HashMap<Node, Double>();
        Map<Node, Double> f_score = new HashMap<Node, Double>();
        Map<Node, Node> paths = new HashMap<Node, Node>();

        g_score.put(start, 0d);
        f_score.put(start, g_score.get(start) + heuristic.estimateDistance(start, target));
        open.set(start, f_score.get(start));

        while (!open.isEmpty()) {
            Node current = null;

            // find the node with lowest f_score value
            double min_f_score = Double.POSITIVE_INFINITY;
            for (Entry<Node, Double> entry : f_score.entrySet()) {
                if (!closed.contains(entry.getKey()) && entry.getValue() < min_f_score) {
                    min_f_score = entry.getValue();
                    current = entry.getKey();
                }
            }

            if (current.equals(target)) return reconstructPath(paths, target);

            open.remove(current);
            closed.add(current);

            for (Node neighbor : current.getAdjacentNodes()) {
                if (closed.contains(neighbor)) {
                    continue;
                }
                double tentative_g_score = g_score.get(current) + current.getDistance(neighbor);

                if (!open.contains(neighbor) || tentative_g_score < g_score.get(neighbor)) {
                    paths.put(neighbor, current);
                    g_score.put(neighbor, tentative_g_score);
                    f_score.put(neighbor, g_score.get(neighbor) + heuristic.estimateDistance(neighbor, target));
                    if (!open.contains(neighbor)) {
                        open.set(neighbor, f_score.get(neighbor));
                    }
                }
            }
        }
        throw new RuntimeException("no path between " + start + " and " + target);
    }
}

Source code of Node.java

public class Node {
    private Map<Node, Double> distances = new HashMap<Node, Double>();

    public final String       name;

    public Node(String name) {
        this.name = name;
    }

    public Set<Node> getAdjacentNodes() {
        return Collections.unmodifiableSet(distances.keySet());
    }

    public double getDistance(Node node) {
        return distances.get(node);
    }

    public void setDistance(Node node, double distance) {
        distances.put(node, distance);
    }

    @Override
    public String toString() {
        return (name == null ? "Node@" + Integer.toHexString(hashCode()) : name);
    }
}

Source code of PriorityQueue.java

public class PriorityQueue<T> {
    transient ArrayList<PriorityEntry<T>> elements     = null;

    private static final int              DEFAULT_SIZE = 10;

    public PriorityQueue() {
        elements = new ArrayList<PriorityEntry<T>>(DEFAULT_SIZE);
    }

    public PriorityQueue(int initialCapacity) {
        elements = new ArrayList<PriorityEntry<T>>(initialCapacity);
    }

    public boolean push(T element, double priority) {
        PriorityEntry<T> entry = new PriorityEntry<T>(element, priority);
        if (elements.contains(entry)) return false;
        elements.add(entry);
        elements.sort(null);
        return true;
    }

    public void set(T element, double priority) {
        PriorityEntry<T> entry = new PriorityEntry<T>(element, priority);
        int index = elements.indexOf(entry);
        if (index >= 0) {
            elements.get(index).setPriority(priority);
        } else {
            elements.add(entry);
        }
        elements.sort(null);
    }

    public T peek() {
        return size() <= 0 ? null : elements.get(0).getValue();
    }

    public T pop() {
        return size() <= 0 ? null : elements.remove(0).getValue();
    }

    public boolean remove(T element) {
        return elements.remove(new PriorityEntry<T>(element, 0));
    }

    public int size() {
        return elements.size();
    }

    public boolean isEmpty() {
        return elements.isEmpty();
    }

    public boolean contains(T element) {
        return elements.contains(new PriorityEntry<T>(element, 0));
    }

    private class PriorityEntry<E> implements Comparable<PriorityEntry<? extends T>> {
        private final E value;
        private double  priority = Double.MIN_VALUE;

        public PriorityEntry(E value, double priority) {
            this.value = value;
            this.priority = priority;
        }

        public E getValue() {
            return value;
        }

        public double getPriority() {
            return priority;
        }

        public void setPriority(double priority) {
            this.priority = priority;
        }

        @Override
        @SuppressWarnings("unchecked")
        public boolean equals(Object o) {
            if (!(o instanceof PriorityEntry)) return false;
            PriorityEntry<?> entry = (PriorityEntry<?>) o;
            return value.equals(entry);
        }

        @Override
        public int compareTo(PriorityEntry<? extends T> entry) {
            return (int) (getPriority() - entry.getPriority());
        }
    }
}
22
  • What exactly do you mean by "irregular distances"?
    – Codor
    Commented Dec 2, 2014 at 12:07
  • @Codor "irregular distances" as in the distance between two nodes is not always the same. Commented Dec 2, 2014 at 12:10
  • 2
    A* is sensitive to the used heuristic. Are you using an admissible one?
    – kiheru
    Commented Dec 2, 2014 at 12:16
  • 1
    @flashdrive2049 So your question boils down to: How does one create an admissible heuristic for an arbitrary graph?
    – Ordous
    Commented Dec 2, 2014 at 16:52
  • 1
    @flashdrive2049 Then most of the text in the question is not relevant :) Googling the above will give you 2 questions with answers: One on Programmers.SE with an answer by me: Shortest Path Between Two Nodes in a +10 Million Nodes Graph and one on SO: What algorithms compute directions from point A to point B on a map?. Both of these propose versions of heuristics.
    – Ordous
    Commented Dec 2, 2014 at 17:10

4 Answers 4

1

Before trying to define a heuristic function for your problem, consider that in many cases, using a poor (or incorrect) estimation of the cost to the goal is as self-defeating as not using an heuristic at all.

In the case of a graph with weighted arcs, you need to consider if there is some information in the nodes which can lead to obtain the heuristic values (for example, if your nodes are cities, a good estimation can be the lenght of the straight line between them; or if your nodes are arrays, a similarity measurement between them). If your nodes are only labels and there is no information that you can use to obtain your heuristic values, maybe the best solution is not using a heuristic at all. This is not the worst scenario for most problems of this type. It is better to use a Dijkstra search (which is the same of A* but using heuristic=0), letting the algorithm expand the nodes based on the cost from the start, than using a bad heuristic which is not consistent, because in this situation you might be expanding unncecesary nodes that seem to be promising based on a bad estimation of the cost to the goal.

I don't know how big are your graphs, but for most problems there is not a significant difference in computation time between using a heuristic and don't using it at all. Specially in the case of a bad heuristic.

I can see that you have your own implementation of A*. I recommend you to take a look of an heuristic search library like Hipster. This library allows you to define your graph and test different search algorithms to know the best one that fits your problem. Some code examples describe exactly your case: seach in weighted directed graphs. It might be useful for your problem.

I hope my answer helps. Regards,

1

Without going into other possible problems I would like to point out the main problem - you lack the plane. In case of shortest distance problem between cities, you have

  • node - city
  • weight - numerical value describing cost to get from city a to city b
  • plane - describes environment ex: city position (in your square grid)

From plane you can extrapolate meaningful heuristic. For example you can make assumption from city position like city with lowest arithmetical distance should be looked first.

If you do not have a plane you do not have any means to predict a meaningful heuristic. A* will still work, but it hardly differs from exhaustive search. You can create plane from the weight, but it .

Ex:

Iterate over weights and find the weight quantiles 20/60/20 - now you have a relative plane

- good weight threshold (lowest 20%) 
- average weight threshold (middle 60%)
- bad weight threshold (highest 20%)

With priority queue your algorithm would pick good moves, then average and finally bad ones. If you want you can have more then 3 segments.


Just as a reminder: A* returns good enough result fast. Using exhaustive search you can find the best solution, but it would become exponentially slower if problem size grows.

0

To add to @kiheru comment above. Your solution will only be as good as the heuristic provided.

If the following line and the heuristic.estimate, has too narrow of a scope. The algorithm will quickly reach a local minimum. Alternatively, if the heuristic isn't admissible the algorithm will result in either no solution or an incorrect random solution.

    f_score.put(start, g_score.get(start) + heuristic.estimateDistance(start, target));

Take a close look at your heuristic and confirm it's admissible. If it is admissible, it may need to be improved in order to provide a more accurate estimate.

3
  • I know that the search algorithm is just as good as the heuristic provided. What I don't understand is why the line you quoted has a too narrow scope. Commented Dec 2, 2014 at 14:36
  • Sorry for the confusion. I don't know that the line has too narrow of a scope. My intention, was to point out the following. As I understood the question, you are getting poor results from the algorithm. Since most of your code follows the Pseudo on Wikipedia, the issue likely resides in the heuristic function. If the heuristic is giving bad/poor results, A* will not provide an optimal solution.
    – agreene
    Commented Dec 2, 2014 at 14:57
  • I know that the problem is the heuristic. The only thing I need is actually the heuristic. The closest neighbor heuristic (see comments on the question) didn't work out well (no surprise) and the Manhattan heuristic doesn't work at all because (see comments on BrendanM's answer) the coordinates of my Node (which were removed by now) are in no relation to the actual distance between two nodes. Commented Dec 2, 2014 at 16:12
0

In the case of your node class it seems to have an X and Y if these represent the node's position in a 2D space maybe you could use a heuristic based on the straight line distance between the nodes calculated from the X and Y values.

2
  • You're right, the node contains 2D coordinates. This is still from a version where I used the Nodes to represent a square in a square grid, I should probably remove that to make it more clear. The problem about calculating the distance by coordinate is that the distance between two nodes has nothing to do with their relative coordinates. Commented Dec 2, 2014 at 16:06
  • Ah ok probably shouldnt have assumed they related to the node position
    – BrendanM
    Commented Dec 2, 2014 at 16:11

Not the answer you're looking for? Browse other questions tagged or ask your own question.