У меня есть таблица с идентификаторами и их соседями, и мне нужно создать рекурсивную функцию, которая находит все возможные пути от начального идентификатора до конечного идентификатора, не пересекая одни и те же точки дважды. Предположим, что начальный идентификатор равен 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 шага.
@ruakh Мне дали этот github.com/Syncleus/dANN-core/blob/v2.x/src/main/java/com/…, но он кажется слишком сложным для чего-то такого миниатюрного.
Чего именно вы здесь ожидаете? Вы просите нас переписать код, чтобы делать то, что вы хотите? Если это задание, как это поможет вам учиться? Дело в том, чтобы вы выполняли работу сами. Не помогают ли многочисленные описания Дейкстры в сети?
@JimGarrison Это не задание, а простой Java-проект, который я усложнил, потому что он казался слишком простым.
Вы можете легко изменить мой код, чтобы он возвращал все пути, если вы просто не позволите ему преждевременно завершить цикл for. Вместо этого позвольте ему собрать каждый действительный результат в список списков и вернуть его ....
@royjr Кстати, все в комментариях правы, вы все равно хотите A *, поскольку, как вы выразились в последнем вопросе, вам нужны все пути, но вы хотите, чтобы они были отсортированы от самого короткого до самого длинного. A * обеспечивает это.
Кстати, он также упустил некоторые важные детали, например, взвешенные края, с которыми он имеет дело.
@ Джеффри Я не совсем понимаю, что вы имеете в виду под взвешенным.
Взвешенный @royjr означает, что существует «Стоимость», связанная с переходом от одного узла к следующему. Типичным примером может служить фактическая длина дороги, соединяющей две путевые точки. Когда мы говорим о кратчайшем пути, мы в основном имеем в виду «наименьшую стоимость».
Нет никакой причины использовать 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);
}
}
Спасибо .. вот что я подумал
A * может найти кратчайший путь ИЛИ n-й кратчайший путь. Например, если вы хотите найти 10 кратчайших путей, A * по-прежнему имеет смысл. Если вы всегда хотите найти все пути, то в этом нет большого преимущества. @royjr это действительно просто сводится к тому, если вам всегда нужны ВСЕ пути, или если вам действительно просто нужно подмножество путей, упорядоченных по расстоянию.
Я должен добавить, что даже если вы хотите найти ВСЕ пути, A * по-прежнему является лучшим выбором, если вы хотите, чтобы эти пути были упорядочены от самого короткого до самого длинного. Только время A * действительно не имеет преимущества, если вы хотите использовать все пути, а не упорядочиваете их.
@JeffreyPhillipsFreeman, у вас есть доказательства этого утверждения? Я считаю невероятно маловероятным, что A * будет иметь какое-либо преимущество перед единственной сортировкой по стоимости пути после того, как будут найдены все решения. И это увеличивает сложность реализации по сравнению с несортированным исчерпывающим поиском.
@sprinter Я, наверное, смогу найти что-нибудь достаточно легкое. Но учитывая, что это можно написать в дюжине или двух строках, я не уверен, что согласен с тем, что это значительно сложнее. Я много раз переписывал алгоритм за свою карьеру, и это никогда не занимало больше нескольких минут. Настоящий вопрос, который вам нужно задать: можете ли вы придумать алгоритм, который может делать это быстрее или в то же время, и есть ли у вас ссылки на это?
@JeffreyPhillipsFreeman, пожалуйста. Мне было бы интересно увидеть, что вы бы использовали в качестве эвристической функции, учитывая, что узлы расположены меньше. В любом случае я добавил простую реализацию исчерпывающего поиска, чтобы вы могли сравнить.
@sprinter Я добавил к своему ответу ниже реализацию A *, которая конкретно решает его проблему. Из реализации должно быть очевидно, почему это более эффективно. К сожалению, на этот раз он упустил из своего вопроса то, что он действительно заботится о сортировке по длине пути. Что сделало бы ваш алгоритм неэффективным в этом отношении.
@sprinter Узлы не занимают позицию меньше. Он просто очень плохо сформулировал свой вопрос. У них есть пространственные координаты. С учетом сказанного A * с эвристикой 0 работает нормально, затем он становится алгоритмом Дейкстры. Что еще более эффективно при сортировке по длине пути.
@JeffreyPhillipsFreeman нет, он все еще сломан. Попробуйте переместить последний вызов addAdjacency
в начало, и вы обнаружите, что получаете только 2 решения. Я предлагаю вам удалить ответ и выполнить некоторую отладку, а затем повторно опубликовать.
@sprinter Я просто попробовал это, без ошибок, мой код все еще выполняется и дает точно такой же, правильный результат. Убедитесь сами: jdoodle.com/a/ukx В этом коде есть предложенные вами изменения, он работает нормально и по-прежнему дает правильный результат. Вы можете выполнить его по предоставленной ссылке.
Вы можете изменить мой исходный код, чтобы вернуть все пути в виде списка, как вы просили. Только не возвращай код раньше срока. Однако это не будет сортироваться по длине пути. Если вы этого хотите, вам понадобится 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 больше не найдено.
Или, если это линия addAdjacency(adjacency, 1,2,1,4,1,5,1);
, тогда будет найдено только 1-4.
Но если это `addAdjacency (смежность, 1,4,1,2,1,5,1); 1, тогда он найдет их все.
@sprinter Спасибо, что заметили это. Я только что исправил ошибку и отредактировал свой ответ. Теперь он должен работать отлично.
да, похоже, сработало. Я сравнил два решения для графа с 17 узлами, 8 связями на узел и 2 126 061 решение. Оба решения дали одинаковые результаты. Исчерпывающее решение с последующей сортировкой заняло 2132 мс, а решение A * - 7708 мс. Это включало упрощение вашего решения, чтобы избавиться от класса соседей (учитывая, что все ссылки в любом случае стоили 1). Я могу отправить вам весь код, который я использовал, если вы хотите взглянуть.
@sprinter A * будет лучше, только если вы рассчитываете стоимость сортировки по длине пути. Сортировали ли вы свой код по длине пути? Если вы используете свой код для получения всех путей, а затем отсортируете длину пути, ваше решение будет значительно медленнее. Преимущество моего решения в том, что оно автоматически сортируется по кратчайшему пути без дополнительных затрат. Это именно то, что нужно OP.
Да, я добавил сортировку по длине пути. Это почти мгновенно: сортировка 2M целых чисел - очень-очень быстрая операция в Java. Преимущество исчерпывающего решения в том, что сортировка выполняется только один раз.
@sprinter конечно, поделитесь своим кодом теста, я бы хотел его увидеть. Мне также было бы интересно узнать об оптимизации пространства, хотя я понимаю, если вы не проверяли это.
Позвольте нам продолжить обсуждение в чате.
@JeffreyPhillipsFreeman Интересно ... как мне добавить список целых чисел, используя addAdjacency, например ad Adjacency (смежность, arrayList)?
@JeffreyPhillipsFreeman ближе всего, что я смог найти, был следующий addAdjacency(adjacency, nnn.get(0),nnn.get(1),nnn.get(2),nnn.get(3),nnn.get(4));
@royjr, чтобы добавить смежность, вы делаете addAdjacency(map, sourceId, destination1Id, cost1Id, destination2Id, cost2Id, defination3Id, cost3Id...)
Стоимость - это расстояние до дороги, представленной краем, или сложность перехода по ней, или что-то подобное. Если вас это не волнует, установите значение 1.
Используйте плохой хак .. но он работает! 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, этот "хакер" не сработает. вам нужно добавить стоимость смежности после каждой смежности ... используйте 1, если у вас нет значимой стоимости. Также, что такое nnn точно ... посмотрите на пример, я показываю примеры того, как добавлять смежности на основе вашей смежности в OP .. он идет "пункт назначения, вес", чередующийся вперед и назад. здесь вес означает то же, что и стоимость.
@royjr такая строка addAdjacency(foo, 1,2,1,3,1,4,1)
говорит: есть смежность от 1 до 2 с весом / стоимостью 1, есть смежность от 1 до 3 со стоимостью 1, есть смежность от 1 до 4 со стоимостью из 1.
Позвольте нам продолжить обсуждение в чате.
Почему вы не хотите использовать A *?