NodeData存储AStar
算法所需的节点的所有信息。此信息包括ghf的值。
但是,所有3个变量的值都取决于源和目标,因此可以在运行时获取。

@param <T>



I正在寻找有关优化,准确性和最佳做法的评论。

final class NodeData<T> { 

    private final T nodeId;
    private final Map<T, Double> heuristic;

    private double g;  // g is distance from the source
    private double h;  // h is the heuristic of destination.
    private double f;  // f = g + h 

    public NodeData (T nodeId, Map<T, Double> heuristic) {
        this.nodeId = nodeId;
        this.g = Double.MAX_VALUE; 
        this.heuristic = heuristic;
    }

    public T getNodeId() {
        return nodeId;
    }

    public double getG() {
        return g;
    }

    public void setG(double g) {
        this.g = g;
    }

    public void calcF(T destination) {
        this.h = heuristic.get(destination);
        this.f = g + h;
    } 

    public double getH() {
        return h;
    }

    public double getF() {
        return f;
    }
 }

/**
 * The graph represents an undirected graph. 
 * 
 * @author SERVICE-NOW\ameya.patil
 *
 * @param <T>
 */
final class GraphAStar<T> implements Iterable<T> {
    /*
     * A map from the nodeId to outgoing edge.
     * An outgoing edge is represented as a tuple of NodeData and the edge length
     */
    private final Map<T, Map<NodeData<T>, Double>> graph;
    /*
     * A map of heuristic from a node to each other node in the graph.
     */
    private final Map<T, Map<T, Double>> heuristicMap;
    /*
     * A map between nodeId and nodedata.
     */
    private final Map<T, NodeData<T>> nodeIdNodeData;

    public GraphAStar(Map<T, Map<T, Double>> heuristicMap) {
        if (heuristicMap == null) throw new NullPointerException("The huerisic map should not be null");
        graph = new HashMap<T, Map<NodeData<T>, Double>>();
        nodeIdNodeData = new HashMap<T, NodeData<T>>();
        this.heuristicMap = heuristicMap;
    } 

    /**
     * Adds a new node to the graph.
     * Internally it creates the nodeData and populates the heuristic map concerning input node into node data.
     * 
     * @param nodeId the node to be added
     */
    public void addNode(T nodeId) {
        if (nodeId == null) throw new NullPointerException("The node cannot be null");
        if (!heuristicMap.containsKey(nodeId)) throw new NoSuchElementException("This node is not a part of hueristic map");

        graph.put(nodeId, new HashMap<NodeData<T>, Double>());
        nodeIdNodeData.put(nodeId, new NodeData<T>(nodeId, heuristicMap.get(nodeId)));
    }

    /**
     * Adds an edge from source node to destination node.
     * There can only be a single edge from source to node.
     * Adding additional edge would overwrite the value
     * 
     * @param nodeIdFirst   the first node to be in the edge
     * @param nodeIdSecond  the second node to be second node in the edge
     * @param length        the length of the edge.
     */
    public void addEdge(T nodeIdFirst, T nodeIdSecond, double length) {
        if (nodeIdFirst == null || nodeIdSecond == null) throw new NullPointerException("The first nor second node can be null.");

        if (!heuristicMap.containsKey(nodeIdFirst) || !heuristicMap.containsKey(nodeIdSecond)) {
            throw new NoSuchElementException("Source and Destination both should be part of the part of hueristic map");
        }
        if (!graph.containsKey(nodeIdFirst) || !graph.containsKey(nodeIdSecond)) {
            throw new NoSuchElementException("Source and Destination both should be part of the part of graph");
        }

        graph.get(nodeIdFirst).put(nodeIdNodeData.get(nodeIdSecond), length);
        graph.get(nodeIdSecond).put(nodeIdNodeData.get(nodeIdFirst), length);
    }

    /**
     * Returns immutable view of the edges
     * 
     * @param nodeId    the nodeId whose outgoing edge needs to be returned
     * @return          An immutable view of edges leaving that node
     */
    public Map<NodeData<T>, Double> edgesFrom (T nodeId) {
        if (nodeId == null) throw new NullPointerException("The input node should not be null.");
        if (!heuristicMap.containsKey(nodeId)) throw new NoSuchElementException("This node is not a part of hueristic map");
        if (!graph.containsKey(nodeId)) throw new NoSuchElementException("The node should not be null.");

        return Collections.unmodifiableMap(graph.get(nodeId));
    }

    /**
     * The nodedata corresponding to the current nodeId.
     * 
     * @param nodeId    the nodeId to be returned
     * @return          the nodeData from the 
     */ 
    public NodeData<T> getNodeData (T nodeId) {
        if (nodeId == null) { throw new NullPointerException("The nodeid should not be empty"); }
        if (!nodeIdNodeData.containsKey(nodeId))  { throw new NoSuchElementException("The nodeId does not exist"); }
        return nodeIdNodeData.get(nodeId);
    }

    /**
     * Returns an iterator that can traverse the nodes of the graph
     * 
     * @return an Iterator.
     */
    @Override public Iterator<T> iterator() {
        return graph.keySet().iterator();
    }
}

public class AStar<T> {

    private final GraphAStar<T> graph;


    public AStar (GraphAStar<T> graphAStar) {
        this.graph = graphAStar;
    }

    // extend comparator.
    public class NodeComparator implements Comparator<NodeData<T>> {
        public int compare(NodeData<T> nodeFirst, NodeData<T> nodeSecond) {
            if (nodeFirst.getF() > nodeSecond.getF()) return 1;
            if (nodeSecond.getF() > nodeFirst.getF()) return -1;
            return 0;
        }
    } 

    /**
     * Implements the A-star algorithm and returns the path from source to destination
     * 
     * @param source        the source nodeid
     * @param destination   the destination nodeid
     * @return              the path from source to destination
     */
    public List<T> astar(T source, T destination) {
        /**
         * http://stackoverflow.com/questions/20344041/why-does-priority-queue-has-default-initial-capacity-of-11
         */
        final Queue<NodeData<T>> openQueue = new PriorityQueue<NodeData<T>>(11, new NodeComparator()); 

        NodeData<T> sourceNodeData = graph.getNodeData(source);
        sourceNodeData.setG(0);
        sourceNodeData.calcF(destination);
        openQueue.add(sourceNodeData);

        final Map<T, T> path = new HashMap<T, T>();
        final Set<NodeData<T>> closedList = new HashSet<NodeData<T>>();

        while (!openQueue.isEmpty()) {
            final NodeData<T> nodeData = openQueue.poll();

            if (nodeData.getNodeId().equals(destination)) { 
                return path(path, destination);
            }

            closedList.add(nodeData);

            for (Entry<NodeData<T>, Double> neighborEntry : graph.edgesFrom(nodeData.getNodeId()).entrySet()) {
                NodeData<T> neighbor = neighborEntry.getKey();

                if (closedList.contains(neighbor)) continue;

                double distanceBetweenTwoNodes = neighborEntry.getValue();
                double tentativeG = distanceBetweenTwoNodes + nodeData.getG();

                if (tentativeG < neighbor.getG()) {
                    neighbor.setG(tentativeG);
                    neighbor.calcF(destination);

                    path.put(neighbor.getNodeId(), nodeData.getNodeId());
                    if (!openQueue.contains(neighbor)) {
                        openQueue.add(neighbor);
                    }
                }
            }
        }

        return null;
    }


    private List<T> path(Map<T, T> path, T destination) {
        assert path != null;
        assert destination != null;

        final List<T> pathList = new ArrayList<T>();
        pathList.add(destination);
        while (path.containsKey(destination)) {
            destination = path.get(destination);
            pathList.add(destination);
        }
        Collections.reverse(pathList);
        return pathList;
    }


    public static void main(String[] args) {
        Map<String, Map<String, Double>> hueristic = new HashMap<String, Map<String, Double>>();
        // map for A    
        Map<String, Double> mapA = new HashMap<String, Double>();
        mapA.put("A",  0.0);
        mapA.put("B", 10.0);
        mapA.put("C", 20.0);
        mapA.put("E", 100.0);
        mapA.put("F", 110.0);


        // map for B
        Map<String, Double> mapB = new HashMap<String, Double>();
        mapB.put("A", 10.0);
        mapB.put("B",  0.0);
        mapB.put("C", 10.0);
        mapB.put("E", 25.0);
        mapB.put("F", 40.0);


        // map for C
        Map<String, Double> mapC = new HashMap<String, Double>();
        mapC.put("A", 20.0);
        mapC.put("B", 10.0);
        mapC.put("C",  0.0);
        mapC.put("E", 10.0);
        mapC.put("F", 30.0);


        // map for X
        Map<String, Double> mapX = new HashMap<String, Double>();
        mapX.put("A", 100.0);
        mapX.put("B", 25.0);
        mapX.put("C", 10.0);
        mapX.put("E",  0.0);
        mapX.put("F", 10.0);

        // map for X
        Map<String, Double> mapZ = new HashMap<String, Double>();
        mapZ.put("A", 110.0);
        mapZ.put("B",  40.0);
        mapZ.put("C",  30.0);
        mapZ.put("E", 10.0);
        mapZ.put("F",  0.0);

        hueristic.put("A", mapA);
        hueristic.put("B", mapB);
        hueristic.put("C", mapC);
        hueristic.put("E", mapX);
        hueristic.put("F", mapZ);

        GraphAStar<String> graph = new GraphAStar<String>(hueristic);
        graph.addNode("A");
        graph.addNode("B");
        graph.addNode("C");
        graph.addNode("E");
        graph.addNode("F");

        graph.addEdge("A", "B",  10);
        graph.addEdge("A", "E", 100);
        graph.addEdge("B", "C", 10);
        graph.addEdge("C", "E", 10);
        graph.addEdge("C", "F", 30);
        graph.addEdge("E", "F", 10);

        AStar<String> aStar = new AStar<String>(graph);

        for (String path : aStar.astar("A", "F")) {
            System.out.println(path);
        }
    }
}


评论

原因:如果(!openQueue.contains(neighbor)){openQueue.add(neighbor); }仅当tentativeG

#1 楼

这是相当不错且外观专业的代码。我真的很喜欢许多小方面:


使用Collections.unmodifiableMap代替浅表副本是很不错的。
使用通用nodeId很聪明,可以编写精美的代码。
所有公共方法中都有输入检查(嗯,有一些例外:仅检查AStarNodeData构造函数,NodeComparator#compareAStar#astar)。
您可以完美地使用空行来分隔不相关的行块。

但是有些方面使您的代码有时很难遵循:


缺少诸如heuristic这样的某些部分的封装:这是什么? Map<T, Map<T, Double>>?我不能为Heuristic<T>实例提供很好的自文档访问器吗?
NodeData中的顺序耦合:每当我setG时,我也必须调用calcF。您可以通过在其中拍一个return this而不是void方法来简化此过程,但是真正的解决方案是摆脱public calcF并使它成为privateNodeData类自行负责维护其不变性,因此对setG的任何调用都应更新相关字段。

命名错误。字母gfh在A *的上下文中具有特定含义,在这里可以。当然,最好包含指向该算法的Wikipedia页面的链接,以便将来的维护者可以理解为什么使用g而不是distance

但是节点之间没有距离,节点或边缘有重量。在优化问题的背景下,通常会谈论成本-这个术语在您的代码中不会一次出现。

它称为heuristic,而不是hueristic。打字错误很容易纠正,应该在还很年轻的时候就予以纠正(HTTP请求中的referer字段的来源应在此处列出)。


有一些格式化“错误”可以通过自动格式化程序轻松纠正。例如。当条件在单行上时,例如if (cond) { single_statement; },请不要使用花括号-删除花括号可减少行噪。否则,您也可以将语句放在单独的行上。

您的某些行过长,应该将其拆分(另请参见下一个技巧)。

尽管您的输入检查值得称赞,但它们确实增加了视觉上的混乱。考虑将它们隐藏在辅助方法之后,例如heuristic.assertContains(nodeId)或最好是Assert.nonNull(nodeId, "node")(假定整个类专用于输入检查)。反对此论点的方法没有那么有用的堆栈跟踪,并且降低了性能(方法调用开销,编译器优化更加困难),但是赞成论点包括更多的自文档化,简洁的代码。

其他说明:


您的文档没有说明从起点到目的地没有路径时会发生什么(例如,如果节点在未连接的子图中)。此处的实现非常清晰:返回了null而不是列表。

A * Wikipedia页面上给出的伪代码使用稍有不同的条件来更新path并可能向openQueue添加邻居:

if (!openQueue.contains(neighbor) || tentativeG < neighbor.getG())`


使用if (tentativeG < neighbor.getG())的地方。可能值得检查权威来源,确定正确的条件是什么。

您可以在组件边界处的T实例与连续范围的int之间进行转换(换句话说:在内部,每个nodeId都是整数)。使用整数可以实现更有效的数据结构,例如数组。这将删除大多数Map查找,但也删除NodeData类。缺点是之后的代码看起来像C。我只是尝试进行转换,然后看看(a)性能是否有明显的提高,以及(b)增加的丑度是否确实值得。


评论


\ $ \ begingroup \ $
A * Wikipedia页面上给出的伪代码使用稍有不同的条件来更新路径,并可能在openQueue中添加邻居。这是一个很大的收获,但我认为我的this.g = Double.MAX_VALUE;正在照顾它。
\ $ \ endgroup \ $
– JavaDeveloper
2014年1月11日下午4:12

\ $ \ begingroup \ $
缺少像启发式这样的某些部分的封装:这是什么Map >?我不能为Heuristic 实例提供很好的自文档访问器吗? -非常好
\ $ \ endgroup \ $
– JavaDeveloper
2014年1月11日4:14



#2 楼

该代码看起来还可以,但是以我的拙见,我认为有些事情可以改善您的代码:


为什么每个节点都有一个启发式表?最好只使用节点来封装成本和分数,但是让算法为您计算f成本。

同样,f计算既嵌入在节点中,也嵌入在AStar算法中。为什么不使用其他组件来计算f?让节点充当数据的“容器”。我认为只在f中计算AStar更好,而不是在NodeAStar中进行类似的计算:

        // You are calculating g here, why f is computed in the node?
        // Responsabilities of each component are not clear
        double distanceBetweenTwoNodes = neighborEntry.getValue();
        double tentativeG = distanceBetweenTwoNodes + nodeData.getG();

        if (tentativeG < neighbor.getG()) {
            neighbor.setG(tentativeG);
            neighbor.calcF(destination); // confusing


AStar只能使用与AStarGraph。这段代码有点束缚。您可以定义一个为状态计算后继函数的函数,因此可以使用graph.edgesFrom代替transitions.from(state)。可以使用您的图形结构或其他任何东西来计算此函数。

检查此实现以查看其中的一些想法;它可能会给您一些提示,以实现更好的A *版本。

#3 楼

我喜欢您的代码,但我认为有一个错误:在neighbor.setG(tentativeG)中,您可以更新可能已经插入到openQueue中的节点的优先级。后者是java PriorityQueue,它不支持优先级更新。在更改对象在队列中的优先级而不调整其位置(根据情况进行筛选)时,可能会破坏其结构。为了使订购者能够在Java PriorityQueue中安全地执行更新,您应该删除节点(O(N)),对其进行更新,然后将其重新插入。
在性能方面,您已经支付了O(N)(这里N是openQueue.contains(neighbor)中开放集的典型大小,因此删除将与此相当。我认为您可以通过例如避免检查优先级队列,而是将记录保留在单独的哈希表上来改善这一点。但是,这不能解决在更新节点之前删除节点时应支付的O(N)。一种可能的解决方法是避免删除过时的节点,而仅添加具有更新优先级的新节点。如果这样做,请确保按某个ID(而不是对象本身)对close集合进行哈希处理,以便在轮询过时的节点时(该节点的ID与已经访问过的新ID相同,因为后者具有更高的优先级)
这种方法的代价是您通常会有一个较长的队列,但是操作最多只能是O(log(N))。

#4 楼

原因:

if (!openQueue.contains(neighbor)) {
     openQueue.add(neighbor);
}


仅当tentativeG < neighbor.getG()吗?

在通用A *伪代码中,无论如何都要进行更新。

例如在Wikipedia上,我尝试运行代码,它发现最佳解决方案只是“感谢” mzivi提到的其他错误。