【问题标题】:Creating a complete list of valid paths via recursion通过递归创建有效路径的完整列表
【发布时间】:2018-04-30 23:20:53
【问题描述】:

我有一个包含 id 及其邻居的表,需要创建一个递归函数来查找从起始 id 到结束 id 的所有可能的路径,而不会两次越过相同的点。假设起始 id 为 1,结束 id 为 3。

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

@Jeffrey Phillips Freeman 编写的当前 sn-p 运行良好,只是它只返回一个可能的路径,而不是 (1,2,3) 和 (1,5,3) 的所有可能路径。我被告知 A* 算法最适合这种情况,但我仍然想创建有效路径列表,让我从 A 点到 B 点而不走那条路线。新的 sn-p 需要简单地将 all 有效路径放在 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 我收到了这个github.com/Syncleus/dANN-core/blob/v2.x/src/main/java/com/…,但对于这种微型规模的东西来说似乎有点太复杂了。
  • 您对这里的期望到底是什么?您是否要求我们重写代码以执行您想要的操作?如果这是一项作业,它将如何帮助您学习?关键是你自己做这项工作。网络上对 Dijkstra 的许多描述都没有帮助吗?
  • @JimGarrison 这不是一个作业,而是一个简单的 Java 项目,我把它弄复杂了,因为它看起来太简单了。
  • @royjr weighted 表示从一个节点遍历到下一个节点存在“成本”。一个典型的例子是连接两个航路点的道路的实际长度。当我们谈论最短路径时,我们基本上是指“最低成本”

标签: java algorithm recursion path-finding


【解决方案1】:

完全没有理由使用 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 我可能会找到一些足够简单的东西。但是考虑到它可以写成十几行或两行,我不确定我是否同意它相当复杂。在我的职业生涯中,我多次重写了这个算法,而且从来没有超过几分钟。您需要问的真正问题是,您能想出一种可以更快或同时完成的算法吗?您有相关参考吗?
【解决方案2】:

您可以修改我的原始代码以按照您的要求将所有路径作为列表返回。只是不要提前返回代码。这不会按路径长度排序,但是,如果需要,则需要 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(adjacency, 1,4,1,2,1,5,1);1 那么它会找到所有这些。
  • @sprinter 感谢您的关注。我刚刚修复了错误并编辑了我的答案。现在应该可以完美运行了。
  • 是的,这似乎有效。我比较了具有 17 个节点、每个节点 8 个链接和 2,126,061 个解决方案的图的两种解决方案。两种解决方案都给出了完全相同的结果。详尽的然后排序解决方案花费了 2132 毫秒,而 A* 解决方案花费了 7708 毫秒。这包括简化您的解决方案以摆脱邻居类(假设所有链接的成本都为 1)。如果你想看看,我可以把我用过的所有代码发给你。
猜你喜欢
  • 2010-10-30
  • 1970-01-01
  • 1970-01-01
  • 2016-02-19
  • 2020-02-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多