Самый короткий путь (наименьшие узлы) для невзвешенного графика

Я пытаюсь создать метод, который возвращает кратчайший путь от одного node к другому в невзвешенном графике. Я считал использование Dijkstra, но это кажется немного излишним, поскольку мне нужна только одна пара. Вместо этого я реализовал поиск по ширине, но проблема в том, что мой возвращающийся список содержит некоторые из узлов, которые мне не нужны - как я могу изменить свой код для достижения своей цели?

public List<Node> getDirections(Node start, Node finish){
    List<Node> directions = new LinkedList<Node>();
    Queue<Node> q = new LinkedList<Node>();
    Node current = start;
    q.add(current);
    while(!q.isEmpty()){
        current = q.remove();
        directions.add(current);
        if (current.equals(finish)){
            break;
        }else{
            for(Node node : current.getOutNodes()){
                if(!q.contains(node)){
                    q.add(node);
                }
            }
        }
    }
    if (!current.equals(finish)){
        System.out.println("can't reach destination");
    }
    return directions;
}

Ответ 1

На самом деле ваш код не будет завершен в циклических графах, рассмотрим график 1 → 2 → 1. У вас должен быть некоторый массив, в котором вы можете указать, какой из node вы уже посетили. А также для каждого node вы можете сохранить предыдущие узлы, из которых вы пришли. Итак, вот правильный код:

private Map<Node, Boolean>> vis = new HashMap<Node, Boolean>();

private Map<Node, Node> prev = new HashMap<Node, Node>();

public List getDirections(Node start, Node finish){
    List directions = new LinkedList();
    Queue q = new LinkedList();
    Node current = start;
    q.add(current);
    vis.put(current, true);
    while(!q.isEmpty()){
        current = q.remove();
        if (current.equals(finish)){
            break;
        }else{
            for(Node node : current.getOutNodes()){
                if(!vis.contains(node)){
                    q.add(node);
                    vis.put(node, true);
                    prev.put(node, current);
                }
            }
        }
    }
    if (!current.equals(finish)){
        System.out.println("can't reach destination");
    }
    for(Node node = finish; node != null; node = prev.get(node)) {
        directions.add(node);
    }
    directions.reverse();
    return directions;
}

Ответ 2

Спасибо, Гиолеква!

Я переписал его, рефакторинг некоторых:

  • Коллекция посещенных узлов не обязательно должна быть картой.
  • Для восстановления пути следующий node может быть просмотрен вместо предыдущего node, устраняя необходимость в обратном направлении.
public List<Node> getDirections(Node sourceNode, Node destinationNode) {
    //Initialization.
    Map<Node, Node> nextNodeMap = new HashMap<Node, Node>();
    Node currentNode = sourceNode;

    //Queue
    Queue<Node> queue = new LinkedList<Node>();
    queue.add(currentNode);

    /*
     * The set of visited nodes doesn't have to be a Map, and, since order
     * is not important, an ordered collection is not needed. HashSet is 
     * fast for add and lookup, if configured properly.
     */
    Set<Node> visitedNodes = new HashSet<Node>();
    visitedNodes.add(currentNode);

    //Search.
    while (!queue.isEmpty()) {
        currentNode = queue.remove();
        if (currentNode.equals(destinationNode)) {
            break;
        } else {
            for (Node nextNode : getChildNodes(currentNode)) {
                if (!visitedNodes.contains(nextNode)) {
                    queue.add(nextNode);
                    visitedNodes.add(nextNode);

                    //Look up of next node instead of previous.
                    nextNodeMap.put(currentNode, nextNode);
                }
            }
        }
    }

    //If all nodes are explored and the destination node hasn't been found.
    if (!currentNode.equals(destinationNode)) {
        throw new RuntimeException("No feasible path.");
    }

    //Reconstruct path. No need to reverse.
    List<Node> directions = new LinkedList<Node>();
    for (Node node = sourceNode; node != null; node = nextNodeMap.get(node)) {
        directions.add(node);
    }

    return directions;
}

Ответ 3

На самом деле не проще получить ответ только для одной пары, чем для всех пар. Обычный способ вычисления кратчайшего пути - начать, как и вы, но делать заметку всякий раз, когда вы сталкиваетесь с новым node и записываете предыдущий node на пути. Затем, когда вы достигнете цели node, вы можете следить за обратными ссылками к источнику и получить путь. Итак, удалите directions.add(current) из цикла и добавьте код примерно так:

Map<Node,Node> backlinks = new HashMap<Node,Node>();

в начале, а затем в цикле

if (!backlinks.containsKey(node)) {
    backlinks.add(node, current);
    q.add(node);
}

а затем, в конце концов, просто создайте список directions в обратном порядке, используя карту backlinks.

Ответ 4

Каждый раз через ваш цикл вы вызываете

directions.Add(current);

Вместо этого вы должны перенести это в место, где вы действительно знаете, что хотите эту запись.

Ответ 5

Вы должны включить родительский node в каждый node, когда вы поместите их в очередь. Затем вы можете просто рекурсивно прочитать путь из этого списка.

Скажите, что вы хотите найти кратчайший путь от A до D на этом графике:

     /B------C------D
   /                |
 A                 /
   \             /
     \E---------

Каждый раз, когда вы вставляете node, следите за тем, как вы сюда попали. Поэтому на этапе 1 B (A) E (A) помещается в очередь. На шаге два B отбрасывается, а C (B) помещается в очередь и т.д. Затем легко найти свой путь назад, просто вернув "назад".

Лучший способ - это, вероятно, сделать массив, если есть узлы и содержать ссылки там (что обычно делается в случае с Dijkstra).