Навигатор A * дает неоптимальный путь

Я написал указатель пути на 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 в качестве родительского параметра, он становится своим собственным родителем. Вот почему цикл построения пути останавливается, когда в функции buildPath выполняется условие «lcnode == lcnode.getParent()», поскольку первый расширенный узел является родителем самого себя. Путь состоит из частей направления, которые состоят из координат x и y, каждая из которых равна -1, 0 или 1. Причина этого в том, что я хотел, чтобы путь вел к цели по относительным координатам. Нет проверки границ карты, это сделано намеренно. Я заменил это, сделав граничные узлы непроходимыми.

Другая картинка:

На этот раз получается хорошо. Разница связана с порядком, в котором я расширяю узлы вокруг последнего закрытого узла, поскольку я ищу закрытый узел с наименьшей стоимостью следующим образом:

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

Если я изменю неравенство на «‹=», проблема на первом изображении будет устранена, а на втором испорчено.

Кстати, я немного расширил A*, чтобы, если пути нет, он получал узел с наименьшей стоимостью H из закрытого списка и запускал другой поиск, нацеленный на этот узел. Таким образом, он приближается к цели, даже если в настоящее время нет пути к целевому узлу.

введите здесь описание изображения

Я не включил все классы, я думаю, что другие не имеют отношения к этой проблеме. Если что-то неясно, я их поддержу, хотя не хотел делать вопрос слишком длинным для чтения.

Насколько я знаю, теория диктует, что последовательная эвристика гарантирует расширение узла с оптимальной стоимостью, поэтому я пока не мог понять, как исправить неточность. Я сделал какие-либо ошибки в коде? Если нет, то как я могу исправить проблему?


РЕДАКТИРОВАТЬ: я включил часть недостающего кода, чтобы прояснить ситуацию:

Класс Направление:

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

узел класса:

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

person PEC    schedule 25.09.2013    source источник
comment
Хороший вопрос, но это слишком много кода. Вы должны проследить его с помощью отладчика (или вставить журнал отладки, чтобы распечатать процесс принятия решения) и выяснить, где именно он отклоняется от ваших ожиданий, а затем, если вы все еще что-то не понимаете, опубликуйте другой вопрос.   -  person Jim Garrison    schedule 25.09.2013


Ответы (2)


Вот псевдокод из Википедии для 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. Если сосед текущего узла уже находится в открытом наборе, вам может потребоваться обновить его g-показатель и родительский узел.

Это верно как для непротиворечивых, так и для несогласованных эвристик. Последовательная эвристика дает вам не возможность пропустить эту проверку, а возможность не ставить в очередь узлы, которые уже были развернуты (т. е. которые находятся в закрытом состоянии). установить).


LinkedList<Node> openList   = new LinkedList<Node>();

openList должен быть PriorityQueue по заказу g(x) + h(x) . Таким образом, вы получите гораздо лучшую производительность.

Кстати, я немного расширил A*, чтобы, если пути нет, он получал узел с наименьшей стоимостью H из закрытого списка и запускал другой поиск, нацеленный на этот узел.

Нет необходимости запускать второй поиск; см. Настройка AStar для поиска ближайшего местоположения к недоступному пункту назначения.

person BlueRaja - Danny Pflughoeft    schedule 25.09.2013
comment
очередь с приоритетом - хорошая идея, но я не думаю, что связанный список что-то испортит, так как я запускаю поиск в открытом списке для участника с наименьшей стоимостью. Может быть медленнее, но все равно находит самый дешевый. Кроме того, какой бы ни была эвристика, узел в любом случае будет соседом своего родителя, вот как работает расширение. - person PEC; 25.09.2013
comment
Я проверил ссылку на настройку AStar. Собственно, это была моя первая идея. Однако, поскольку целевой узел и ближайший достижимый узел к цели находятся в разных местах, эвристика немного отличается, и вы можете получить неоптимальный путь, если отступите от ближайшего узла, не выполняя еще один поиск, поскольку он является узел цели. - person PEC; 25.09.2013
comment
@PEC Я не вижу, где в вашем коде вы ищете элемент с самой низкой стоимостью. И ваш комментарий к другому ответу (58 появляется в открытом списке перед узлом под ним, поэтому 58 выбирается и помещается в закрытый список) предполагает, что вы либо этого не делаете, либо делаете неправильно. 58 не должен расширяться перед узлом под ним, так как он должен иметь более высокое значение f. - person BlueRaja - Danny Pflughoeft; 25.09.2013
comment
У него нет более высокого значения f, я написал небольшое объяснение выше. Также поиск узла с наименьшей стоимостью f происходит в функции поиска в классе ProxyAStar. Это первый цикл for. - person PEC; 25.09.2013
comment
@PEC Ты прав, я ошибался. Я обновил свой ответ. И Re, поскольку целевой узел и ближайший достижимый узел к цели находятся в разных местах, вы можете получить неоптимальный путь: это неверно. Если A* не может найти путь до конца, он заполнит карту и предоставит вам оптимальный путь к каждой достижимой точке (он должен быть оптимальным; в противном случае, если мы нашел путь до конца, проходящий через эту точку, это было бы неоптимально!) - person BlueRaja - Danny Pflughoeft; 25.09.2013
comment
Я думаю, вы правы. Раньше, когда я пытался это сделать, у меня не было части повторного воспитания, включенной в код, поэтому я получил неоптимальный путь. Теперь, похоже, он работает, просто возвращаясь к ближайшему узлу в закрытом списке. Спасибо за совет. - person PEC; 25.09.2013

Вы уверены, что ваша эвристика допустима? Я нашел код:

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, т. е. недопустимо (учитывая, что вы хотите получить кратчайший путь).

person nonDucor    schedule 25.09.2013
comment
-1 эта эвристика мне кажется правильной. Ваш пример неверен, так как расстояние не будет равно 1000,05 (он не допускает движения под любым углом...) - person BlueRaja - Danny Pflughoeft; 25.09.2013
comment
Поскольку алгоритм обрабатывает только диагональные и ортогональные перемещения, в худшем сценарии H равно оставшемуся расстоянию в этом сценарии карты сетки. Под этим я подразумеваю расстояние только диагональными и ортогональными шагами. Должен ли я сделать неравенство верным для евклидова расстояния? - person PEC; 25.09.2013
comment
@PEC Как рассчитывается реальная стоимость? Какова реальная стоимость диагонального хода и реальная стоимость ортогонального хода? Судя по картинкам, они имеют одинаковую стоимость (поэтому вы получаете зависимость от порядка, в котором оцениваются узлы, и это объясняет разницу между первым и последним примером) - person nonDucor; 25.09.2013
comment
диагональ: 14, ортогональный: 10. Думаю, я знаю, в чем проблема в части, где что-то идет не так: в основном, узел 58 и тот, что ниже, имеют одинаковые значения F, и 58 появляется в открытом списке перед узлом ниже (оба расширяются с 72), поэтому 58 выбирается и помещается в закрытый список, таким образом становясь родителем 44. Таким образом, 44 достигается неоптимальным путем. Эта проблема, хотя и может быть устранена путем повторной проверки лучших кандидатов-родителей, не должна существовать, если эвристика непротиворечива (монотонна). - person PEC; 25.09.2013
comment
@PEC - Это то же самое, о чем я думаю. И это произошло бы, если бы и диагональные, и ортогональные движения имели одинаковую реальную стоимость. Я думаю, что это было бы результатом getG. Можете ли вы показать эту часть кода? - person nonDucor; 25.09.2013
comment
@nonDuctor - я понятия не имею, как разместить здесь код, поэтому я включил код, который вы просили выше, в вопрос. - person PEC; 25.09.2013
comment
@PEC: в основном, узел 58 и следующий за ним имеют одинаковые значения F - это неверно. 58-узел имеет более высокую g-стоимость и h-стоимость, поэтому их сумма (f-стоимость) также будет выше. - person BlueRaja - Danny Pflughoeft; 25.09.2013
comment
@BlueRaja-DannyPflughoeft - 58 имеет g-стоимость 58 (по совпадению то же самое, что и число, нарисованное на узле, поскольку 58 - это середина пути) и h-стоимость 50. Это становится f-стоимостью 108 , Узел ниже 58 имеет g-стоимость 54 и h-стоимость 54, что также дает 108. Чем более длинный шаг требуется для достижения 58, тем ниже h-стоимость, которую он получает за него. Узел 58 и узел под ним имеют одинаковые значения F. - person PEC; 25.09.2013