Создание полного списка допустимых путей с помощью рекурсии

У меня есть таблица с идентификаторами и их соседями, и мне нужно создать рекурсивную функцию, которая находит все возможные пути от начального идентификатора до конечного идентификатора, не пересекая одни и те же точки дважды. Предположим, что начальный идентификатор равен 1, а конечный - 3.

{1 | 2,5}
{2 | 1,3,4,5}
{3 | 2,5}
{4 | 2}
{5 | 1,2,3}

Этот текущий фрагмент, написанный @ Джеффри Филлипс Фриман, работал довольно хорошо, за исключением того, что он возвращал только один возможный путь вместо всех возможных путей, которые были бы (1,2,3) и (1,5,3). Мне сообщили, что в этой ситуации лучше всего подойдет алгоритм A *, но я все же хотел бы создать список допустимых путей, которые приведут меня из точки A в точку B, не переходя по этому маршруту. Новый фрагмент должен просто поместить все действительные пути в ArrayList. Я намерен использовать ArrayList для определения наилучшего пути, учитывая как длину пути, так и другие факторы. Таким образом, ArrayList, отсортированный по расстоянию пути, будет бонусом. В качестве дополнительного примечания к узлам в реальной задаче привязаны пространственные координаты, однако путь между узлами не всегда является прямым.

List<Integer> searchHops(int from, int to, List<Integer> seen) {
    seen.add(from);

    if (from == to)
        return new ArrayList<Integer>(Arrays.asList(from));

    for (int neighbor : getNeighbors(from))

        if (!seen.contains(neighbor)) {
            List<Integer> result = searchHops(neighbor, to, seen);

            if (result != null) {
                result.add(0, from);
                return result;
            }
        }

    return null;
}

У меня около 200 очков, и в текущем состоянии простой тест из точки A в точку B (всего в одной точке) отправляет меня в путешествие на 22 шага.

Почему вы не хотите использовать A *?

ruakh 01.05.2018 01:27

@ruakh Мне дали этот github.com/Syncleus/dANN-core/blob/v2.x/src/main/java/com/…, но он кажется слишком сложным для чего-то такого миниатюрного.

royjr 01.05.2018 01:31

Чего именно вы здесь ожидаете? Вы просите нас переписать код, чтобы делать то, что вы хотите? Если это задание, как это поможет вам учиться? Дело в том, чтобы вы выполняли работу сами. Не помогают ли многочисленные описания Дейкстры в сети?

Jim Garrison 01.05.2018 01:42

@JimGarrison Это не задание, а простой Java-проект, который я усложнил, потому что он казался слишком простым.

royjr 01.05.2018 03:54

Вы можете легко изменить мой код, чтобы он возвращал все пути, если вы просто не позволите ему преждевременно завершить цикл for. Вместо этого позвольте ему собрать каждый действительный результат в список списков и вернуть его ....

Jeffrey Phillips Freeman 01.05.2018 04:27

@royjr Кстати, все в комментариях правы, вы все равно хотите A *, поскольку, как вы выразились в последнем вопросе, вам нужны все пути, но вы хотите, чтобы они были отсортированы от самого короткого до самого длинного. A * обеспечивает это.

Jeffrey Phillips Freeman 01.05.2018 05:01

Кстати, он также упустил некоторые важные детали, например, взвешенные края, с которыми он имеет дело.

Jeffrey Phillips Freeman 01.05.2018 07:25

@ Джеффри Я не совсем понимаю, что вы имеете в виду под взвешенным.

royjr 01.05.2018 13:39

Взвешенный @royjr означает, что существует «Стоимость», связанная с переходом от одного узла к следующему. Типичным примером может служить фактическая длина дороги, соединяющей две путевые точки. Когда мы говорим о кратчайшем пути, мы в основном имеем в виду «наименьшую стоимость».

Jeffrey Phillips Freeman 01.05.2018 20:44
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
9
1 030
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Нет никакой причины использовать A *. Он предназначен для максимально эффективного поиска кратчайшего пути. Если вы хотите найти все пути независимо от длины, A * будет накладными расходами без каких-либо преимуществ.

В псевдокоде ваш алгоритм должен выглядеть примерно так:

findPaths for path:
    if path is complete
        add to solution set
    else
        for each link from last step that is not in path
            add next step to path
            call findPaths
            remove next step from path

В настоящий момент вы возвращаете путь. Если вы хотите найти все пути, вам нужно сохранить их в списке путей.

Вот пример реализации:

public class FindPath {
    private final Stack<Integer> path = new Stack<>();
    private final Map<Integer, Set<Integer>> links = new HashMap<>();

    public void addLink(int from, int to) {
        links.putIfAbsent(from, new HashSet<>());
        links.get(from).add(to);
    }

    public void find(int from, int to, Consumer<Stack<Integer>> action) {
        path.push(from);
        if (from == to)
            action.accept(path);
        else
            links.getOrDefault(from, Set.of()).stream()
                    .filter(s -> !path.contains(s))
                    .forEach(s -> find(s, to, action));
        path.pop();
    }

    public static void main(String[] args) {
        FindPath finder = new FindPath();
        Random rand = new Random();
        IntStream.range(0, 20).forEach(n -> rand.ints(7, 0, 20).forEach(t -> finder.addLink(n, t)));
        finder.find(0, 19, System.out::println);
    }
}

Спасибо .. вот что я подумал

royjr 01.05.2018 03:53

A * может найти кратчайший путь ИЛИ n-й кратчайший путь. Например, если вы хотите найти 10 кратчайших путей, A * по-прежнему имеет смысл. Если вы всегда хотите найти все пути, то в этом нет большого преимущества. @royjr это действительно просто сводится к тому, если вам всегда нужны ВСЕ пути, или если вам действительно просто нужно подмножество путей, упорядоченных по расстоянию.

Jeffrey Phillips Freeman 01.05.2018 04:24

Я должен добавить, что даже если вы хотите найти ВСЕ пути, A * по-прежнему является лучшим выбором, если вы хотите, чтобы эти пути были упорядочены от самого короткого до самого длинного. Только время A * действительно не имеет преимущества, если вы хотите использовать все пути, а не упорядочиваете их.

Jeffrey Phillips Freeman 01.05.2018 04:57

@JeffreyPhillipsFreeman, у вас есть доказательства этого утверждения? Я считаю невероятно маловероятным, что A * будет иметь какое-либо преимущество перед единственной сортировкой по стоимости пути после того, как будут найдены все решения. И это увеличивает сложность реализации по сравнению с несортированным исчерпывающим поиском.

sprinter 01.05.2018 06:37

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

Jeffrey Phillips Freeman 01.05.2018 06:43

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

sprinter 01.05.2018 08:01

@sprinter Я добавил к своему ответу ниже реализацию A *, которая конкретно решает его проблему. Из реализации должно быть очевидно, почему это более эффективно. К сожалению, на этот раз он упустил из своего вопроса то, что он действительно заботится о сортировке по длине пути. Что сделало бы ваш алгоритм неэффективным в этом отношении.

Jeffrey Phillips Freeman 01.05.2018 08:15

@sprinter Узлы не занимают позицию меньше. Он просто очень плохо сформулировал свой вопрос. У них есть пространственные координаты. С учетом сказанного A * с эвристикой 0 работает нормально, затем он становится алгоритмом Дейкстры. Что еще более эффективно при сортировке по длине пути.

Jeffrey Phillips Freeman 01.05.2018 08:20

@JeffreyPhillipsFreeman нет, он все еще сломан. Попробуйте переместить последний вызов addAdjacency в начало, и вы обнаружите, что получаете только 2 решения. Я предлагаю вам удалить ответ и выполнить некоторую отладку, а затем повторно опубликовать.

sprinter 01.05.2018 10:07

@sprinter Я просто попробовал это, без ошибок, мой код все еще выполняется и дает точно такой же, правильный результат. Убедитесь сами: jdoodle.com/a/ukx В этом коде есть предложенные вами изменения, он работает нормально и по-прежнему дает правильный результат. Вы можете выполнить его по предоставленной ссылке.

Jeffrey Phillips Freeman 01.05.2018 10:08
Ответ принят как подходящий

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

public List<List<Integer>> searchHops(int from, int to, Set<Integer> seen) {
    seen.add(from);

    if (from == to) {
        final List<List<Integer>> newList = new ArrayList<>();
        newList.add(new ArrayList<>(Arrays.asList(from)));
        return newList;
    }

    List<List<Integer>> allPaths = null;
    for (int neighbor : getNeighbors(from)) {
        if (!seen.contains(neighbor)) {
            List<List<Integer>> results = searchHops(neighbor, to, new HashSet<>(seen));

            if (results != null) {
                for(List<Integer> result : results) {
                    result.add(0, from);
                    if ( allPaths != null )
                        allPaths.add(result);
                }
                if ( allPaths == null )
                    allPaths = results;
            }
        }
    }
    return allPaths;
}

Если вы действительно заботитесь о том, чтобы упорядочить свои пути от кратчайшего пути к самому длинному пути, было бы гораздо лучше использовать A *. A * вернет столько возможных путей, сколько вы хотите, в порядке кратчайшего пути. Итак, если вам действительно нужны все возможные пути, упорядоченные от самого короткого до самого длинного, вам все равно нужен алгоритм A *. Код, который я предложил выше, будет намного медленнее, чем должен быть, если вы заботитесь о порядке от самого короткого к самому длинному, не говоря уже о том, что он займет больше места, чем вы хотели бы, чтобы сохранить все возможные пути сразу.

Поскольку вы указали, что сначала заботитесь о кратчайшем пути и, возможно, захотите получить N-кратчайшие пути, вам обязательно следует использовать здесь A *.

Если вам нужна реализация на основе A *, способная возвращать все пути, упорядоченные от самого короткого до самого длинного, это выполнит следующее. У него есть несколько преимуществ. Во-первых, он эффективен при сортировке от самого короткого до самого длинного. Кроме того, он вычисляет каждый дополнительный путь только при необходимости, поэтому, если вы остановитесь раньше, потому что вам не нужен каждый отдельный путь, вы сэкономите время обработки. Он также повторно использует данные для последующих путей каждый раз, когда вычисляет следующий путь, что делает его более эффективным. В целом алгоритм должен быть наиболее эффективным, если вы заботитесь о сортировке по длине пути.

import java.util.*;

public class AstarSearch {
    private final Map<Integer, Set<Neighbor>> adjacency;
    private final int destination;

    private final NavigableSet<Step> pending = new TreeSet<>();

    public AstarSearch(Map<Integer, Set<Neighbor>> adjacency, int source, int destination) {
        this.adjacency = adjacency;
        this.destination = destination;

        this.pending.add(new Step(source, null, 0));
    }

    public List<Integer> nextShortestPath() {
        Step current = this.pending.pollFirst();
        while( current != null) {
            if ( current.getId() == this.destination )
                return current.generatePath();
            for (Neighbor neighbor : this.adjacency.get(current.id)) {
                if (!current.seen(neighbor.getId())) {
                    final Step nextStep = new Step(neighbor.getId(), current, current.cost + neighbor.cost + predictCost(neighbor.id, this.destination));
                    this.pending.add(nextStep);
                }
            }
            current = this.pending.pollFirst();
        }
        return null;
    }

    protected int predictCost(int source, int destination) {
        return 0; //Behaves identical to Dijkstra's algorithm, override to make it A*
    }

    private static class Step implements Comparable<Step> {
        final int id;
        final Step parent;
        final int cost;

        public Step(int id, Step parent, int cost) {
            this.id = id;
            this.parent = parent;
            this.cost = cost;
        }

        public int getId() {
            return id;
        }

        public Step getParent() {
            return parent;
        }

        public int getCost() {
            return cost;
        }

        public boolean seen(int node) {
            if (this.id == node)
                return true;
            else if (parent == null)
                return false;
            else
                return this.parent.seen(node);
        }

        public List<Integer> generatePath() {
            final List<Integer> path;
            if (this.parent != null)
                path = this.parent.generatePath();
            else
                path = new ArrayList<>();
            path.add(this.id);
            return path;
        }

        @Override
        public int compareTo(Step step) {
            if (step == null)
                return 1;
            if ( this.cost != step.cost)
                return Integer.compare(this.cost, step.cost);
            if ( this.id != step.id )
                return Integer.compare(this.id, step.id);
            if ( this.parent != null )
                this.parent.compareTo(step.parent);
            if (step.parent == null)
                return 0;
            return -1;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Step step = (Step) o;
            return id == step.id &&
                cost == step.cost &&
                Objects.equals(parent, step.parent);
        }

        @Override
        public int hashCode() {
            return Objects.hash(id, parent, cost);
        }
    }

   /*******************************************************
   *   Everything below here just sets up your adjacency  *
   *   It will just be helpful for you to be able to test *
   *   It isnt part of the actual A* search algorithm     *
   ********************************************************/

    private static class Neighbor {
        final int id;
        final int cost;

        public Neighbor(int id, int cost) {
            this.id = id;
            this.cost = cost;
        }

        public int getId() {
            return id;
        }

        public int getCost() {
            return cost;
        }
    }

    public static void main(String[] args) {
        final Map<Integer, Set<Neighbor>> adjacency = createAdjacency();
        final AstarSearch search = new AstarSearch(adjacency, 1, 4);
        System.out.println("printing all paths from shortest to longest...");
        List<Integer> path = search.nextShortestPath();
        while(path != null) {
            System.out.println(path);
            path = search.nextShortestPath();
        }
    }

    private static Map<Integer, Set<Neighbor>> createAdjacency() {
        final Map<Integer, Set<Neighbor>> adjacency = new HashMap<>();

        //This sets up the adjacencies. In this case all adjacencies have a cost of 1, but they dont need to. Otherwise
        //They are exactly the same as the example you gave in your question
        addAdjacency(adjacency, 1,2,1,5,1);         //{1 | 2,5}
        addAdjacency(adjacency, 2,1,1,3,1,4,1,5,1); //{2 | 1,3,4,5}
        addAdjacency(adjacency, 3,2,1,5,1);         //{3 | 2,5}
        addAdjacency(adjacency, 4,2,1);             //{4 | 2}
        addAdjacency(adjacency, 5,1,1,2,1,3,1);     //{5 | 1,2,3}

        return Collections.unmodifiableMap(adjacency);
    }

    private static void addAdjacency(Map<Integer, Set<Neighbor>> adjacency, int source, Integer... dests) {
        if ( dests.length % 2 != 0)
            throw new IllegalArgumentException("dests must have an equal number of arguments, each pair is the id and cost for that traversal");

        final Set<Neighbor> destinations = new HashSet<>();
        for(int i = 0; i < dests.length; i+=2)
            destinations.add(new Neighbor(dests[i], dests[i+1]));
        adjacency.put(source, Collections.unmodifiableSet(destinations));
    }
}

Результатом приведенного выше кода является следующее:

[1, 2, 4]
[1, 5, 2, 4]
[1, 5, 3, 2, 4]

Обратите внимание, что каждый раз, когда вы вызываете nextShortestPath(), он по запросу генерирует для вас следующий кратчайший путь. Он только рассчитывает необходимые дополнительные шаги и не пересекает старые пути дважды. Более того, если вы решите, что вам не нужны все пути и завершите выполнение раньше, вы сэкономите значительное время на вычисления. Вы вычисляете только то количество путей, которое вам нужно, и не более того.

Если у вас есть какая-то эвристика, которая помогает оценить стоимость пути, переопределите метод predictCost() и поместите его туда. Вы упомянули, что ваши узлы также имеют связанные с ними пространственные координаты. Хорошей эвристикой в ​​этом случае будет евклидово расстояние между двумя узлами (расстояние между ними будет прямой линией). Однако это полностью вариант и помогает сократить время вычислений только в том случае, если вы выйдете до вычисления всех возможных путей.

Наконец, следует отметить, что алгоритмы A * и Dijkstra имеют некоторые незначительные ограничения, хотя я не думаю, что это повлияет на вас. А именно, это не будет работать правильно на графике с отрицательными весами.

Вот ссылка на JDoodle, где вы можете запустить код самостоятельно в браузере и увидеть, как он работает. Вы также можете изменить график, чтобы показать, что он работает и на других графиках: http://jdoodle.com/a/ukx

Попробуйте добавить ссылку с 1 по 4 (строка станет addAdjacency(adjacency, 1,2,1,5,1,4,1);). Решение 1-2-4 больше не найдено.

sprinter 01.05.2018 10:59

Или, если это линия addAdjacency(adjacency, 1,2,1,4,1,5,1);, тогда будет найдено только 1-4.

sprinter 01.05.2018 11:02

Но если это `addAdjacency (смежность, 1,4,1,2,1,5,1); 1, тогда он найдет их все.

sprinter 01.05.2018 11:03

@sprinter Спасибо, что заметили это. Я только что исправил ошибку и отредактировал свой ответ. Теперь он должен работать отлично.

Jeffrey Phillips Freeman 01.05.2018 11:12

да, похоже, сработало. Я сравнил два решения для графа с 17 узлами, 8 связями на узел и 2 126 061 решение. Оба решения дали одинаковые результаты. Исчерпывающее решение с последующей сортировкой заняло 2132 мс, а решение A * - 7708 мс. Это включало упрощение вашего решения, чтобы избавиться от класса соседей (учитывая, что все ссылки в любом случае стоили 1). Я могу отправить вам весь код, который я использовал, если вы хотите взглянуть.

sprinter 01.05.2018 11:38

@sprinter A * будет лучше, только если вы рассчитываете стоимость сортировки по длине пути. Сортировали ли вы свой код по длине пути? Если вы используете свой код для получения всех путей, а затем отсортируете длину пути, ваше решение будет значительно медленнее. Преимущество моего решения в том, что оно автоматически сортируется по кратчайшему пути без дополнительных затрат. Это именно то, что нужно OP.

Jeffrey Phillips Freeman 01.05.2018 11:42

Да, я добавил сортировку по длине пути. Это почти мгновенно: сортировка 2M целых чисел - очень-очень быстрая операция в Java. Преимущество исчерпывающего решения в том, что сортировка выполняется только один раз.

sprinter 01.05.2018 11:43

@sprinter конечно, поделитесь своим кодом теста, я бы хотел его увидеть. Мне также было бы интересно узнать об оптимизации пространства, хотя я понимаю, если вы не проверяли это.

Jeffrey Phillips Freeman 01.05.2018 11:44

Позвольте нам продолжить обсуждение в чате.

sprinter 01.05.2018 11:45

@JeffreyPhillipsFreeman Интересно ... как мне добавить список целых чисел, используя addAdjacency, например ad Adjacency (смежность, arrayList)?

royjr 01.05.2018 20:27

@JeffreyPhillipsFreeman ближе всего, что я смог найти, был следующий addAdjacency(adjacency, nnn.get(0),nnn.get(1),nnn.get(2),nnn.get(3),nnn.get(4));

royjr 01.05.2018 20:31

@royjr, чтобы добавить смежность, вы делаете addAdjacency(map, sourceId, destination1Id, cost1Id, destination2Id, cost2Id, defination3Id, cost3Id...) Стоимость - это расстояние до дороги, представленной краем, или сложность перехода по ней, или что-то подобное. Если вас это не волнует, установите значение 1.

Jeffrey Phillips Freeman 01.05.2018 20:47

Используйте плохой хак .. но он работает! if (nnn.size() == 3) { addAdjacency(adjacency, nnn.get(0), nnn.get(1), nnn.get(2)); } else if (nnn.size() == 5) { и так далее. МНОГО ПУТЕЙ .. почти слишком много Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

royjr 01.05.2018 20:53

@royjr, этот "хакер" не сработает. вам нужно добавить стоимость смежности после каждой смежности ... используйте 1, если у вас нет значимой стоимости. Также, что такое nnn точно ... посмотрите на пример, я показываю примеры того, как добавлять смежности на основе вашей смежности в OP .. он идет "пункт назначения, вес", чередующийся вперед и назад. здесь вес означает то же, что и стоимость.

Jeffrey Phillips Freeman 01.05.2018 20:56

@royjr такая строка addAdjacency(foo, 1,2,1,3,1,4,1) говорит: есть смежность от 1 до 2 с весом / стоимостью 1, есть смежность от 1 до 3 со стоимостью 1, есть смежность от 1 до 4 со стоимостью из 1.

Jeffrey Phillips Freeman 01.05.2018 21:01

Позвольте нам продолжить обсуждение в чате.

royjr 01.05.2018 21:07

Другие вопросы по теме