使用深度优先搜索

时间:2016-05-28 21:09:35

标签: java algorithm search graph depth-first-search

我有一个带有顶点123456的有向图。使用深度优先搜索,如果我想能够找到1-4中的唯一路径数(例如),我将如何进行呢?这是我目前的DFS。

private final Map<Character, Node> mNodes;
private final List<Edge> mEdges;
private List<Node> mVisited = new ArrayList<>();
int weight;
int numberOfPaths;

public DepthFirstSearch(Graph graph){
    mNodes = graph.getNodes();
    mEdges = new ArrayList<>(graph.getEdges());
    for(Node node : mNodes.values()){
        node.setVisited(false);
    }
}

public void depthFirstSearch(Node source){

    source.setVisited(true);
    List<Edge> neighbours = source.getNeighbouringEdges(mEdges);
    for(Edge edge : neighbours){
        System.out.println(edge.getFrom().getName()+"-"+edge.getTo().getName());
        if(!edge.getTo().isVisited()){

            mVisited.add(edge.getTo());
            weight += edge.getWeight();
            depthFirstSearch(edge.getTo());

        }
    }

}

3 个答案:

答案 0 :(得分:9)

由于不允许循环,因此您实际上有 DAG directed acyclid graph)。

以下是与此主题相关的一些问题:

基本上,想法是获取 DAG topological sort,然后迭代从目标节点开始向后到源节点的节点,计算来自该节点的路径数。

由于数组没有循环且节点是拓扑排序的,因此当您访问节点时,可以从该节点到达的所有节点在排序中出现,并且已经被访问过。因此,从节点到目标的路径计数是与其相邻的节点计数的总和。

型号:

class Graph
{
    private List<Node> nodes;
    private List<Edge> edges;

    public Graph() {
        nodes = new ArrayList<>();
        edges = new ArrayList<>();
    }

    public List<Node> getNodes() { return nodes; }    
    public List<Edge> getEdges() { return edges; }

    public void addNode(Node node) { nodes.add(node); }    
    public void addEdge(Edge edge) {
        edges.add(edge);        
        edge.getFrom().addEdge(edge);
        edge.getTo().addEdge(edge);
    }
}

class Node
{
    private List<Edge> outgoings, incomings;

    public Node() {
        outgoings = new ArrayList<>();
        incomings = new ArrayList<>();
    }

    public List<Edge> getOutgoings() { return outgoings; }    
    public List<Edge> getIncomings() { return incomings; }

    public void addEdge(Edge edge) {
        if (edge.getFrom() == this) outgoings.add(edge);
        if (edge.getTo() == this) incomings.add(edge);
    }
}

class Edge
{
    private Node from, to;

    public Edge(Node from, Node to) {
        this.from = from;
        this.to = to;
    }

    public Node getFrom() { return from; }    
    public Node getTo() { return to; }
}

算法:

static int countPaths(Graph graph, Node source, Node target)
{
    List<Node> nodes = topologicalSort(graph);
    int[] counts = new int[nodes.size()];

    int srcIndex = nodes.indexOf(source);
    int tgtIndex = nodes.indexOf(target);

    counts[tgtIndex] = 1;

    for (int i = tgtIndex; i >= srcIndex; i--) {
        for (Edge edge : nodes.get(i).getOutgoings())
            counts[i] += counts[nodes.indexOf(edge.getTo())];
    }

    return counts[srcIndex];
}

static List<Node> topologicalSort(Graph g)
{
    List<Node> result = new ArrayList<>();
    Set<Node> visited = new HashSet<>();
    Set<Node> expanded = new HashSet<>();

    for (Node node: g.getNodes())
        explore(node, g, result, visited, expanded);

    return result;
}

static void explore(Node node, Graph g, List<Node> ordering, Set<Node> visited, Set<Node> expanded) {
    if (visited.contains(node)) {            
        if (expanded.contains(node)) return;
        throw new IllegalArgumentException("Graph contains a cycle.");
    }

    visited.add(node);

    for (Edge edge: node.getIncomings())
        explore(edge.getFrom(), g, ordering, visited, expanded);

    ordering.add(node);
    expanded.add(node);
}

用法:

Graph g = new Graph();

Node a = new Node();
Node b = new Node();
Node c = new Node();
Node d = new Node();
Node e = new Node();

Edge ab = new Edge(a, b);
Edge bc = new Edge(b, c);
Edge cd = new Edge(c, d);
Edge ad = new Edge(a, d);
Edge ae = new Edge(a, e);
Edge ed = new Edge(e, d);
Edge be = new Edge(b, e);
Edge ec = new Edge(e, c);

g.addNode(a);
g.addNode(b);
g.addNode(c);
g.addNode(d);
g.addNode(e);

g.addEdge(ab);
g.addEdge(bc);
g.addEdge(cd);
g.addEdge(ad);
g.addEdge(ae);
g.addEdge(ed);
g.addEdge(be);
g.addEdge(ec);

int count = countPaths(g, a, d); 

//count: 6

// Paths:
//1. A->B->C->D
//2. A->D
//3. A->E->D
//4. A->B->E->C->D
//5. A->B->E->D
//6. A->E->C->D

但是,如果你想使用 BFS 来做这件事,你可以尝试重新启动被访问节点的回溯:

static int countPaths(Graph graph, Node source, Node target)
{
    Set<Node> visiteds = new HashSet<>();
    return BFS(source, target, visiteds);
}

static int BFS(Node current, Node target, Set<Node> visiteds) {
    if (current == target) return 1;
    else
    {
        int count = 0;
        visiteds.add(current);

        for (Edge edge : current.getOutgoings())
            if (!visiteds.contains(edge.getTo()))
                count += BFS(edge.getTo(), target, visiteds);

        visiteds.remove(current);
        return count;
    }
}    

但是,为了提高性能,您可以实现某种memoization

static int countPaths(Graph graph, Node source, Node target)
{
    Set<Node> visiteds = new HashSet<>();
    Map<Node, Integer> memoize = new HashMap<>();

    for (Node node : graph.getNodes())
        memoize.put(node, -1);

    memoize.put(target, 1);

    return BFS(source, visiteds, memoize);
}

static int BFS(Node current, Set<Node> visiteds, Map<Node, Integer> memoize) {
    if (memoize.get(current) != -1) return memoize.get(current);
    else
    {
        int count = 0;
        visiteds.add(current);

        for (Edge edge : current.getOutgoings())
            if (!visiteds.contains(edge.getTo()))
                count += BFS(edge.getTo(), visiteds, memoize);

        visiteds.remove(current);
        memoize.put(current, count);
        return count;
    }
}  

答案 1 :(得分:2)

(假设有向无环图)

您需要做的就是运行DFS并计算通往目标节点的每条路径。

您的代码中的错误是您的setVisited()isVisited()状态是全局的。这意味着无论何时遇到节点C,您都会将其标记为已访问,并且对于整个DFS运行它都会保持标记 - 即使对于实际尚未访问过的路径也是如此。

示例DFS运行(给定简单图A -> BB -> CA -> C):

  • (第1步 - 路径A)访问A,标记A已访问

  • (步骤1.1 - 路径A -> B)访问B,标记B已访问

  • (步骤1.1.1 - 路径A -> B -> C)访问C,标记C已访问

  • (步骤1.2 - 路径A -> C)这里您跳过此路线,因为C被标记为从步骤1.1.1访问(这是错误的)

您需要通过以下方式正确跟踪访问过的节点:

  • 信任调用者输入图实际上是非循环的,并且根本不跟踪被访问的节点(因为在非循环图中无法访问单个节点两次)。您冒着程序输入错误输入的无限递归的风险

  • 使用更简单/更便宜的循环检测。您可以跟踪递归的深度以及何时深度超过图表中的节点总数引发异常

  • 在访问后立即重置节点访问状态(这是@Arturo Menchaca在他的回答中建议的)

  • 为每个正在处理的路线保留单独的访问状态

跟踪列表中访问过的节点的示例java代码(这样可以打印发现的路径):

package test.java.so;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class So37503760 {

    public static class Graph {

        private final Map<Character, Node> nodes = new TreeMap<Character, Node>();

        public Node addNode(char c) {
            Node existingNode = nodes.get(c);
            if(existingNode!=null) {
                return existingNode;
            }
            Node newNode = new Node(c);
            nodes.put(c, newNode);
            return newNode;
        }

        public void addEdge(char f, char t) {
            addNode(f).addChild(addNode(t));
        }

        public Node getNode(char name) {
            Node ret = nodes.get(name);
            if(ret==null) {
                throw new RuntimeException("No such node " + name);
            }
            return ret;
        }

    }

    public static class Node {

        private final char name;
        private final ArrayList<Node> children = new ArrayList<Node>();

        public Node(char c) {
            this.name=c;
        }

        public void addChild(Node childNode) {
            children.add(childNode);
        }

        public ArrayList<Node> getChildren() {
            return children;
        }

        public char getName() {
            return name;
        }
    }

    public static void main(String[] args) {
        Graph graph = new Graph();
        graph.addEdge('A', 'B');
        graph.addEdge('A', 'C');
        graph.addEdge('B', 'C');
        graph.addEdge('C', 'D');
        graph.addEdge('C', 'E');
        graph.addEdge('D', 'E');
        int numberOfPaths = depthFirstSearch(graph, 'A', 'E');
        System.out.println("Found " + numberOfPaths + " paths");
    }

    public static int depthFirstSearch(Graph graph, char startNode, char destinationNode){
        return depthFirstSearch(graph, graph.getNode(startNode), graph.getNode(destinationNode), new ArrayList<Node>());
    }

    public static int depthFirstSearch(Graph graph, Node startNode, Node destinationNode, List<Node> currentPath){
        if(currentPath.contains(startNode)) {
            currentPath.add(startNode);
            throw new RuntimeException("Cycle detected: " + pathToString(currentPath));
        }
        currentPath.add(startNode);
        try {
//          System.out.println("Progress: " + pathToString(currentPath));
            if(startNode==destinationNode) {
                System.out.println("Found path: " + pathToString(currentPath));
                return 1;
            }
            int ret=0;
            for(Node nextNode : startNode.getChildren()){
                ret += depthFirstSearch(graph, nextNode, destinationNode, currentPath);
            }
            return ret;
        } finally {
            currentPath.remove(currentPath.size()-1);
        }
    }

    private static String pathToString(List<Node> path) {
        StringBuilder b = new StringBuilder();
        boolean printArrow=false;
        for(Node n : path) {
            if(printArrow) {
                b.append(" -> ");
            }
            b.append(n.getName());
            printArrow=true;
        }
        return b.toString();
    }
}

答案 2 :(得分:0)

这实际上取决于图表的边缘。如果所有顶点彼此连接,那将非常简单,因为每次都会获得相同数量的路径(1-> 4,4-> 2-> 4,4-> 3- &gt; 4,1-> 5-> 4,...,1-> 6-> 5-> 3-> 2-> 4 [以1至4的路径为例) 。考虑到任何n-> m路径(假设您不需要循环),它看起来非常相似,并且每次都会有相同数量的路径。       但是,如果有任何顶点没有连接到其他顶点,那么它就会变得有趣。您可能想要使用修改后的Djikstra算法,然后为您提供不同的答案(我有点猜测您正在寻找)的唯一路径数。