A*算法中处理节点间距离不规则的启发式方法

6
我目前正在实现一个带有不规则节点间距离的A*算法。包含节点的图是一个有向加权图。每个节点至少连接到另一个节点,也可能存在具有不同距离的对称连接。节点只是一个标签,不包含任何特殊信息。
我需要的是一种启发式的方法来确定从节点A到另一个节点B的最短路径尽可能准确。我尝试过使用一种启发式方法,返回节点最近邻的距离,但当然这并不如没有启发式方法(= Dijkstra)有效。
我的A*算法实现主要由两个类组成,一个用于算法本身(AStar),另一个用于节点(Node)。代码主要基于维基百科上的伪代码。
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);
    }
}

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);
    }
}

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());
        }
    }
}

2
A*算法对所使用的启发式函数很敏感。您是否在使用可被接受的启发式函数 - kiheru
如果你的意思是“从A到B的距离可能与从B到A的距离不同”,那么这对于A*算法来说应该没有问题。最简单的例子是一个非双向图 - “缺失”的边可以被建模为具有无限权重的边。你可能需要更好地解释你的启发式函数以获得帮助。 - Ordous
1
@flashdrive2049 所以你的问题归结为:如何为任意图创建一个可接受的启发式函数? - Ordous
1
@flashdrive2049,那么问题中的大部分文本都不相关 :) 在Google上搜索以上内容会给你两个带有答案的问题:一个是由我回答的Programmers.SE上的问题:在一个1000万节点图中两个节点之间的最短路径,另一个是SO上的问题:有哪些算法可以计算地图上从点A到点B的方向?。这两个问题都提出了启发式算法的版本。 - Ordous
@Ordous好的,非常感谢! - mezzodrinker
显示剩余18条评论
4个回答

1

不谈其他可能的问题,我想指出主要问题——你缺乏飞机。在两个城市之间的最短距离问题中,你需要:

  • 节点 - 城市
  • 权重 - 描述从城市a城市b的成本的数值
  • 平面 - 描述环境,例如城市位置(在你的方形网格中)

从平面中可以推断出有意义的启发式方法。例如,你可以根据城市位置做出假设,比如应该首先查找算术距离最短的城市。

如果你没有平面,就没有任何方法来预测有意义的启发式方法。A*仍然可以工作,但它几乎与穷举搜索没有什么区别。你可以从权重创建平面,但是...

例如:

迭代权重并找到权重分位数20/60/20-现在你有一个相对平面

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

使用优先队列,您的算法将选择好的移动,然后是平均的移动,最后是不好的移动。如果需要,您可以拥有多于3个段落。
提醒一下:A*算法可以快速返回足够好的结果。使用穷举搜索可以找到最佳解决方案,但如果问题规模增大,速度将呈指数级下降。

1
在尝试为您的问题定义启发式函数之前,请考虑到,在许多情况下,使用不良(或不正确)的目标成本估计与根本不使用启发式一样自我毁灭。
对于带有加权弧的图形,您需要考虑是否存在某些节点中的信息可以导致获得启发式值(例如,如果您的节点是城市,则良好的估计可以是它们之间直线长度;或者如果您的节点是数组,则是它们之间的相似度测量)。如果您的节点仅是标签,并且没有可用于获取启发式值的信息,则最好的解决方案可能是根本不使用启发式。对于此类大多数问题而言,这不是最糟糕的情况。最好使用Dijkstra搜索(它与A*相同,但使用启发式= 0),让算法基于从起点的成本扩展节点,而不是使用不一致的坏启发式,因为在这种情况下,您可能会扩展那些看起来很有前途,但基于对目标成本的错误估计而实际上是不必要的节点。
我不知道您的图形有多大,但对于大多数问题而言,在使用启发式和根本不使用之间的计算时间没有显着差异。特别是在使用不良启发式的情况下。
我可以看到你有自己的A*算法实现。我建议你看一下启发式搜索库,例如Hipster。该库允许你定义图形并测试不同的搜索算法,以找到最适合你的问题的算法。一些代码示例恰好描述了你的情况:在加权有向图中进行搜索。它可能对你的问题有用。
我希望我的回答能够帮到你。问候。

0

补充一下@kiheru上面的评论。您的解决方案只会像所提供的启发式一样好。

如果以下行和heuristic.estimate的范围太窄。算法将很快达到局部最小值。或者,如果启发式不可接受,则算法将导致没有解决方案或不正确的随机解决方案。

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

仔细检查你的启发式算法并确认它是可接受的。如果它是可接受的,可能需要改进以提供更准确的估计。


我知道搜索算法的好坏取决于所提供的启发式。但我不明白你引用的那行代码为什么范围太窄。 - mezzodrinker
抱歉造成困惑。我不知道这行代码的范围太窄了。我的意图是指出以下问题。据我理解,您从算法中得到了较差的结果。由于您的大部分代码都遵循维基百科上的伪代码,问题很可能在启发式函数中。如果启发式函数提供了错误/较差的结果,则A*将无法提供最优解。 - agreene
我知道问题出在启发式算法上。我现在需要的是启发式算法。最近邻算法(请参见问题评论)效果不佳(毫不惊讶),曼哈顿启发式算法根本不起作用,因为我的节点坐标(现已删除,请参见BrendanM答案的评论)与两个节点之间的实际距离无关。 - mezzodrinker

0
在你的节点类中,如果它们表示二维空间中节点的位置,则可以基于从 X 和 Y 值计算出的节点之间的直线距离使用启发式方法。

你说得对,该节点包含2D坐标。这仍然是我使用节点表示正方形网格版本的一部分,我应该将其删除以使其更清晰。通过坐标计算距离的问题在于两个节点之间的距离与它们的相对坐标没有任何关系。 - mezzodrinker
啊,好的,可能不应该假设它们与节点位置有关。 - BrendanM

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接