A*路径规划器提供次优路径

3
我用Java写了一个路径搜索算法,大多数情况下它都能良好工作。然而,我发现了一种情况它会出错。我使用的启发式算法应该是一致的,而一致的启发式意味着算法应该总是找到离扩展节点最近的路径,据我所知。

这是问题的图片:

起点是绿色的,数字只表示从每个特定节点到红色目标路径的长度。

我的启发式类:

package heuristics;
import pathcomponents.Direction;


public class Heuristics {

    public static int DO(int x1, int y1, int x2, int y2) {
        int dx = Math.abs(x1 - x2);
        int dy = Math.abs(y1 - y2);
        int D, O;
        if(dx > dy) {
            D = dy;
            O = dx - D;
        }
        else {
            D = dx;
            O = dy - D;
        }
        return D*Direction.D_COST + O*Direction.O_COST;
    }

}

方向.D_COST = 14,方向.O_COST = 10

启发式函数返回以下值:对角线距离*14 + 直线距离*10。

算法:

package pathfinders;


import java.util.LinkedList;
import pathcomponents.Direction;
import pathcomponents.Node;
import pathcomponents.Path;
import heuristics.Heuristics;


public class ProxyAStar {

    static private boolean contains(LinkedList<Node> list, int x, int y) {
        for(Node n : list)
            if(n.getX() == x && n.getY() == y) return true;
        return false;
    }

    public static Path buildPath(Node lcnode) {
        int cost = lcnode.getG();
        LinkedList<Direction> path = new LinkedList<Direction>();
        while(lcnode != lcnode.getParent()) {
            int dx = lcnode.getX() - lcnode.getParent().getX();
            int dy = lcnode.getY() - lcnode.getParent().getY();
            path.add(new Direction(dx, dy));
            lcnode = lcnode.getParent();
        }
        return new Path(path, cost);
    }

    public static Path search(boolean[][] map, int sx, int sy, int gx, int gy) {
        LinkedList<Node> openList   = new LinkedList<Node>();
        LinkedList<Node> closedList = new LinkedList<Node>();
        openList.add(new Node(sx, sy, 0, Heuristics.DO(sx, sy, gx, gy), null));
        while(!openList.isEmpty()) {
            Node lcnode = openList.peekFirst();
            for(Node n : openList) {
                if(n.getCost() < lcnode.getCost()) {
                    lcnode = n;
                }
            }
            int x = lcnode.getX();
            int y = lcnode.getY();
            if(x == gx && y == gy) {
                return buildPath(lcnode);
            }
            closedList.add (lcnode);
            openList.remove(lcnode);
            for(int i = -1; i <= 1; ++i) {
                for(int j = -1; j <= 1; ++j) {
                    int cx = x + i;
                    int cy = y + j;
                    if((i == 0 && j == 0) || map[cx][cy] == false)continue;
                    if(!contains(openList,cx,cy) && !contains(closedList,cx,cy)){
                        openList.add(new Node(cx, cy, lcnode.getG() + Heuristics.DO(x, y, cx, cy), Heuristics.DO(cx, cy, gx, gy), lcnode));
                    }
                }
            }
        }
        Node lcnode = closedList.peekFirst();
        for(Node n : closedList) {
            if(n.getH() < lcnode.getH()) {
                lcnode = n;
            }
        }
        openList   = null;
        closedList = null;
        return search(map, sx, sy, lcnode.getX(), lcnode.getY());
    }
}

类Node有通常的G、H和F成本以及一个父引用。当构造函数接收到null作为parent参数时,它变成自己的父亲。这就是为什么在buildPath函数中遇到“lcnode == lcnode.getParent()”条件时,路径构建循环会停止,因为第一个扩展的节点是其自身的父亲。路径由Direction片段组成,每个片段由x和y坐标组成,每个坐标都是-1、0或1。

这样做的原因是我希望路径通过相对坐标来导向目标。没有地图边界检查,这是有意义的。我通过使边界节点不可行走来代替进行边界检查。

另一张图片:

这次效果非常好。这与我围绕最后一个关闭节点扩展节点的顺序有关,因为我像这样搜索最低成本的关闭节点:

for(Node n : openList) {
    if(n.getCost() < lcnode.getCost()) {
        lcnode = n;
    }
}

如果我将不等式改为"<=", 第一张图片中的问题会得到解决,但第二张图片则会出现混乱。
顺便说一下,我稍微扩展了A*算法,以便在没有路径时,它可以从关闭列表中获取最低的H成本节点,并运行另一个搜索来定位该节点。这样,即使当前没有到达目标节点的路径,它也可以接近目标。

enter image description here

我没有包含每个类,我认为其他类与此问题无关。如果有不清楚的地方,我会包括它们,但是我不想让问题太长难读。
据我所知,理论上一致的启发式可以保证最优代价的节点扩展,因此我还没有弄清楚如何解决不准确性。我的代码有错吗?如果没有,我该如何解决这个问题?
编辑:我包括了一些缺失的代码以澄清问题:

class Direction:

package pathcomponents;
public class Direction {
    public static final int D_COST = 14;
    public static final int O_COST = 10;
    public static int signum(int n) {
        return (n < 0) ? -1 : ((n == 0) ? 0 : 1);
    }
    private final int x, y;
    public Direction(int x, int y) {
        this.x = signum(x);
        this.y = signum(y);
    }
    public Direction(Direction source) {
        this.x = source.x;
        this.y = source.y;
    }
    public int getX() {return x;}
    public int getY() {return y;}
}

类 Node:

package pathcomponents;
public class Node {
    private final int    x, y;
    private       int       G;
    private final int       H;
    private       int       F;
    private       Node parent;
    public Node(int x, int y, int G, int H, Node parent) {
        this.x      =                                x;
        this.y      =                                y;
        this.G      =                                G;
        this.H      =                                H;
        this.F      =                            G + H;
        this.parent = (parent == null) ? this : parent;
    }
    public int  getX()      {return      x;}
    public int  getY()      {return      y;}
    public int  getG()      {return      G;}
    public int  getH()      {return      H;}
    public int  getCost()   {return      F;}
    public Node getParent() {return parent;}
    public void setParent(Node parent, int G) {
        this.parent = parent;
        this.G      =      G;
        F           = G +  H; 
    }
}

1
好问题,但这段代码太长了。你应该使用调试器(或插入调试日志以打印决策过程),并找出它从你的预期中偏离的确切位置,然后如果还有什么你不理解的地方,请发一个新的问题。 - Jim Garrison
2个回答

1
这是维基百科上关于具有一致启发式的A*算法的伪代码:

1. while openset is not empty
2.     current := the node in openset having the lowest f_score[] value
3.     if current = goal
4.        return reconstruct_path(came_from, goal)
5. 
6.     remove current from openset
7.     add current to closedset
8.     for each neighbor in neighbor_nodes(current)
9.        tentative_g_score := g_score[current] + dist_between(current,neighbor)
10.       if neighbor in closedset and tentative_g_score >= g_score[neighbor]
11.          continue
12.
13.       if neighbor not in openset or tentative_g_score < g_score[neighbor] 
14.          came_from[neighbor] := current
15.          g_score[neighbor] := tentative_g_score
16.          f_score[neighbor] := g_score[neighbor] + heuristic_cost_estimate(neighbor, goal)
17.          if neighbor not in openset
18.             add neighbor to openset
19. 
20. return failure

重要的是第13行 - 如果当前节点的邻居已经在openset中,您可能需要更新其g分数和父节点。这对于一致和非一致的启发式算法都是正确的。一致启发式算法给你的不是跳过这个检查的能力,而是不必重新排队已经被扩展的节点(即已经在closed-set中的节点)。
LinkedList<Node> openList   = new LinkedList<Node>();

openList应该是一个PriorityQueue,按照g(x) + h(x)排序。这样可以获得更好的性能。

另外,我对A*进行了一些扩展,如果没有路径,则从闭合列表中获取H成本最低的节点,并运行另一个搜索以针对该节点。

无需运行第二次搜索;请参见Tweaking AStar to find closest location to unreachable destination


优先队列是一个好主意,但我认为链表不会搞砸任何东西,因为我在开放列表中搜索最低成本成员。可能会慢一些,但仍然可以找到最便宜的那个。此外,无论启发式是什么,该节点都将是其父节点的邻居,这就是扩展的工作方式。 - PEC
我检查了优化的AStar链接。实际上,那是我的第一个想法。然而,由于目标节点和最接近可到达目标节点的位置不同,所以启发式函数会有一些偏差,如果你从最接近的节点返回而不运行另一个以该节点为目标节点的搜索,则可能会得到一个次优路径。 - PEC
@PEC 我没有看到你的代码中搜索最低成本成员的部分。而你对另一个答案的评论 (“58出现在下面节点之前的开放列表中,所以58被选中并放入了关闭列表”) 表明你可能没有这样做,或者做错了。58不应该在下面的节点之前扩展,因为它应该有更高的f值。 - BlueRaja - Danny Pflughoeft
它没有更高的f值,我在上面写了一个简短的解释。此外,在类“ProxyAStar”的函数“search”中进行最低f成本节点的搜索。这是第一个for循环。 - PEC
@PEC 你是正确的,我错了。我已经更新了我的答案。关于“由于目标节点和最近可到达目标节点位于不同位置,可能会得到次优路径”的问题:那是不正确的。如果A*找不到通往终点的路径,它将对地图进行泛洪填充,并为每个可到达点提供最佳路径(它必须是最佳的;否则,如果我们找到一条通过该点的通往终点的路径,它将是次优的!) - BlueRaja - Danny Pflughoeft
1
我认为你是正确的。之前当我尝试做这件事时,我的代码中没有包含重新父节点的部分,因此我得到了一个次优路径。现在似乎只需回溯关闭列表中最接近的节点即可正常工作。感谢您的建议。 - PEC

1

您确定您的启发式函数是可行的吗?我找到了代码:

if(dx > dy) {
    D = dy;
    O = dx - D;
} else {
    D = dx;
    O = dy - D;
}
return D*Direction.D_COST + O*Direction.O_COST;

令人惊讶。

特别地,假设dx = 100,dy = 1。那么实际距离是1000.05(考虑到每个正方形具有10个长度单位),而您的启发式估计将会是1004,也就是说,它不是可接受的(考虑到您想获得最短路径)。


1
-1 这个启发式方法在我看来是正确的。你的例子是不正确的,因为距离不会是1000.05(他不允许任意角度的移动...) - BlueRaja - Danny Pflughoeft
@PEC 真实成本是如何计算的?对角线移动的真实成本和正交移动的真实成本各是多少?从图片上看,它们似乎有完全相同的成本(这就是为什么您会获得节点评估顺序的依赖性,并且这解释了第一个和最后一个示例之间的差异)。 - nonDucor
@PEC - 这正是我所思考的。如果对角线移动和正交移动具有相同的实际成本,那么就会发生这种情况。我认为这将是“getG”的结果。你能展示一下代码的这部分吗? - nonDucor
@nonDuctor - 我不知道如何在这里发布代码,所以我在问题中包含了你要求的代码。 - PEC
@BlueRaja-DannyPflughoeft - "58" 的 g-cost 为 58(恰好与节点上绘制的数字相同,因为“58”是路径的中间),h-cost 为 50。这将成为 f-cost 为 108。下面的节点“58”的 g-cost 为 54,h-cost 为 54,也导致 f-cost 为 108。到达“58”所需的较长步骤通过获得更低的 h-cost 得到了回报。 “58”节点和其下方的节点具有相同的 F 值。 - PEC
显示剩余3条评论

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