NodeData
存储AStar
算法所需的节点的所有信息。此信息包括
g
,h
和f
的值。 但是,所有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);
}
}
}
#1 楼
这是相当不错且外观专业的代码。我真的很喜欢许多小方面:使用
Collections.unmodifiableMap
代替浅表副本是很不错的。使用通用
nodeId
很聪明,可以编写精美的代码。 所有公共方法中都有输入检查(嗯,有一些例外:仅检查
AStar
和NodeData
构造函数,NodeComparator#compare
和AStar#astar
)。您可以完美地使用空行来分隔不相关的行块。
但是有些方面使您的代码有时很难遵循:
缺少诸如
heuristic
这样的某些部分的封装:这是什么? Map<T, Map<T, Double>>
?我不能为Heuristic<T>
实例提供很好的自文档访问器吗?NodeData
中的顺序耦合:每当我setG
时,我也必须调用calcF
。您可以通过在其中拍一个return this
而不是void
方法来简化此过程,但是真正的解决方案是摆脱public calcF
并使它成为private
。 NodeData
类自行负责维护其不变性,因此对setG
的任何调用都应更新相关字段。命名错误。字母
g
,f
和h
在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
\ $ \ endgroup \ $
– JavaDeveloper
2014年1月11日4:14
#2 楼
该代码看起来还可以,但是以我的拙见,我认为有些事情可以改善您的代码:为什么每个节点都有一个启发式表?最好只使用节点来封装成本和分数,但是让算法为您计算
f
成本。 同样,
f
计算既嵌入在节点中,也嵌入在AStar
算法中。为什么不使用其他组件来计算f
?让节点充当数据的“容器”。我认为只在f
中计算AStar
更好,而不是在Node
和AStar
中进行类似的计算: // 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提到的其他错误。
评论
原因:如果(!openQueue.contains(neighbor)){openQueue.add(neighbor); }仅当tentativeG