繁体   English   中英

在有向图中查找循环,其中至少一个顶点是特定顶点的邻居(不包括在循环中)

[英]Finding cycle in a directed graph in which at least one vertex is a neighbor of a specific vertex (not included in the cycle)

我正在尝试计算有向图中从顶点 0 到顶点 1 的所有可能路径。 我已经完成了包含非循环图的算法,但我需要检查是否有循环,因为如果有,那么我们可能在图中有无限路径。 (因此路径不必是简单路径)。 仅对试图找到循环的顶点 1 的邻居运行 DFS 是否最佳,然后检查循环中的至少一个顶点是否具有通向顶点 2 的边?

我在想这样的事情:

for every neighbor of vertex 1
    run DFS
if there is a cycle
   for every vertex in cycle
       find if it has an edge leading to vertex 2
else
   run second dfs to count the simple paths

目前我已经完成了第二个 dfs,当图中没有循环时,它基本上找到了所有简单路径。

要检测图中是否存在循环,请按如下方式修改深度优先搜索 (DFS):

- run DFS 
    - IF a node is reached for the second time 
       - IF path exists from node reached again to current DFS node
          - the path is a cycle

此答案 ( https://stackoverflow.com/a/75036736/16582 ) 显示了执行此操作的 C++ 代码。

看起来您必须在顶点 1 和找到的循环中的任何顶点之间有直接连接。 如果该连接不存在,则可以忽略该循环。

因此,运行循环查找算法,以获得循环向量。 遍历循环,丢弃任何不符合您的约束条件的循环。

我认为您不需要单独的循环检测。 当您继续进行主要路径计数搜索时,循环检测可能会自动发生。 这个想法是做一个BFS。

任务可以这样表述:

  1. 有向图G ,由节点{Ni}和边{Ej}组成。
  2. G可以有任何东西:循环、断开连接的组、孤立节点等。
  3. 一个节点称为“初始”,另一个节点称为“最终”。
  4. 目标是计算从节点Ninitial到节点Nfinal的唯一轨迹的数量。

由于其复杂性,应避免使用 DFS。 想想菱形图,它可能导致 DFS 的指数级复杂度:

       N2      N5      N8
     /    \  /    \  /    \    ....
-- N1      N4      N7      N10 
     \    /  \    /  \    /    ....
       N3      N6      N9

该算法的推理是:

  1. Pi为从节点Ninitial到节点Ni的轨迹数。
  2. 设“Lj”为包含边Ej的轨迹数。
  3. 对于任意一条边Ej ,其Lj等于该边起始节点的Pi
  4. 对于任何节点“Ni”,其“Pi”等于所有传入边的Lj之和。
  5. 如果节点的所有传入边的“Lj”都已知,则我们称该节点为“完整”节点。
  6. 假设初始节点是“完整的”,初始条件Pinitial = 1

算法本身是:

  1. 设置节点引用队列。 BFS 将从该队列中一个接一个地挑选节点并处理它们。
  2. 处理某个节点Ni意味着将它的Pi传播到它的所有出边。 结果,图中下方的一些直接相邻节点可能变得“完整”。
  3. 如果在处理某个节点时,其他一些节点变得“完整”,则应将其他节点发布到队列中。

就这样。

在非循环图中,上述算法将迭代所有节点并计算所有可达节点的Pi ,包括Pfinal

在有环的图中,只有图的非环部分会被迭代,因为任何环都不允许它的边(因此节点)变得“完整”。 因此,如果在 BFS 完成后您看到一些节点已被访问,但仍然“不完整”——您知道您有循环(或无法到达的传入边)。

最小可行的例子是:

#include <cstddef>
#include <iostream>
#include <stdexcept>
#include <utility>
#include <vector>


// -------------------------------------------------------------
//              Problem
// -------------------------------------------------------------
using NodeIndex = size_t;

struct Edge
{
    // An directed edge is defined by two nodes: from and to.
    NodeIndex   from;
    NodeIndex   to;
};

class Graph
{
public:
    using Edges = std::vector<Edge>;

    Graph(NodeIndex numNodes, Edges edges)
     : m_numNodes(numNodes)
     , m_edges(std::move(edges))
    {
    }

    NodeIndex GetNumNodes() const
    {
        return m_numNodes;
    }

    const Edges& GetEdges() const
    {
        return m_edges;
    }

private:
    NodeIndex   m_numNodes;
    Edges       m_edges;
};


// -------------------------------------------------------------
//              Solver API
// -------------------------------------------------------------
// Number of paths could grow exponentially with the size of the graph.
// Consider using some library capable of handling arbitrary large integers.
using BigInt = size_t;

BigInt
CountPaths(const Graph& graph, NodeIndex initialNode, NodeIndex finalNode);


// -------------------------------------------------------------
//              Solver implementation
// -------------------------------------------------------------
struct Node
{
    // A node is considered 'complete' if the number of paths from the starting node to this node is already known.
    // Otherwise the node is considered 'incomplete'.

    using EdgeIndex = NodeIndex;
    using ChildIndex = NodeIndex;

    EdgeIndex           m_pending = 0;          // Number of incoming edges from incomplete nodes.
    ChildIndex          m_numChildren = 0;      // Number of outgoing edges, same as the number of child nodes.
    ChildIndex          m_firstLink = 0;        // Index of the first child link.
    ChildIndex          m_lastLink = 0;         // Index of the last child link.
    BigInt              m_numPaths = 0;         // Number of trajectories from the starting node to this node.
};

BigInt
CountPaths(const Graph& graph, NodeIndex initialNode, NodeIndex finalNode)
{
    using EdgeIndex = Node::EdgeIndex;
    using ChildIndex = Node::ChildIndex;
    using Nodes = std::vector<Node>;
    using IndexVector = std::vector<NodeIndex>;

    NodeIndex           numNodes = graph.GetNumNodes();
    EdgeIndex           numEdges = graph.GetEdges().size();
    Nodes               nodes(numNodes);
    IndexVector         queue(numNodes);
    IndexVector         childLinks(numEdges);

    // Calculate the number of incoming ('pending') edges
    // and the number of outgoing ('child') edges for each node.
    // O(E).
    for (const Edge& edge : graph.GetEdges())
    {
        ++nodes[edge.from].m_numChildren;
        ++nodes[edge.to].m_pending;
    }

    // Allocate chunks of 'childLinks' array for child nodes of each node.
    // O(N).
    ChildIndex          offset = 0;
    for (Node& node : nodes)
    {
        node.m_firstLink = offset;
        node.m_lastLink = offset;
        offset += node.m_numChildren;
    }

    // Initialize child links for each node.
    // O(E).
    for (const Edge& edge : graph.GetEdges())
    {
        childLinks[nodes[edge.from].m_lastLink++] = edge.to;
    }

    // Initialize the queue of nodes for BFS.
    size_t      queueTop = 0;

    // Post the starting node to the queue.
    nodes[initialNode].m_numPaths = 1;  // There's only one way to reach the starting node.
    nodes[initialNode].m_pending += 1;  // Prevent the starting node from being posted again. Overflow arelt, though.
    queue[queueTop++] = initialNode;

    EdgeIndex   initialNodePending = nodes[initialNode].m_pending;
    EdgeIndex   finalNodePending = nodes[finalNode].m_pending;

    if (finalNodePending == 0)
    {
        throw std::runtime_error("Final node is unreachable because it has no incoming edges.");
    }

    // Every node that has no incoming edges can be posted to the queue right now.
    // Such nodes are unreachabe, so they contribute 0 to the number of possible paths.
    // Still they should be processed, otherwise some edges could remain "pending" forever.
    // O(N).
    for (NodeIndex i = 0; i < numNodes; ++i)
    {
        if (nodes[i].m_pending == 0)
            queue[queueTop++] = i;
    }

    // Main BFS loop.
    // O(N) * O(E), worst case.
    for (size_t currentIndex = 0; currentIndex < queueTop; ++currentIndex)
    {
        // Pick the first node in the queue.
        const Node&     current = nodes[queue[currentIndex]];

        // We already know how many paths lead to this node.
        std::cout << "Processing node " << queue[currentIndex] << " with path count = " << current.m_numPaths << std::endl;

        // Follow each edge from the current node to the next 'child' node.
        for (NodeIndex linkIndex = current.m_firstLink; linkIndex < current.m_lastLink; ++linkIndex)
        {
            NodeIndex   childIndex = childLinks[linkIndex];
            Node&       child = nodes[childIndex];
            std::cout << "  Child node " << childIndex << " path count = " << child.m_numPaths << "+" << current.m_numPaths << std::endl;
            child.m_numPaths += current.m_numPaths;
            --child.m_pending;

            // If all incoming edges to this child have been accrued,
            // then this child node becomes 'complete' and is ready to be propagated.
            if (child.m_pending == 0)
            {
                // Post it to the queue.
                std::cout << "  Child node " << childIndex << " is ready" << std::endl;
                queue[queueTop++] = childIndex;
            }
        }

        // Visual sugar
        std::cout << std::endl;
    }

    if (initialNodePending != nodes[initialNode].m_pending)
    {
        throw std::runtime_error("Initial node has been visited again. It's part of some loop!");
    }

    // See if all ways to reach the final node have been explored, then the answer is ready.
    if (nodes[finalNode].m_pending == 0)
    {
        return nodes[finalNode].m_numPaths;
    }

    // At this point there could be some loops.
    // But it's not clear if they affect the result.
    nodes[initialNode].m_pending -= 1;

    // Find all blocking nodes and post them to the queue.
    // O(N).
    queueTop = 0;
    for (NodeIndex i = 0; i < numNodes; ++i)
    {
        if ((nodes[i].m_pending != 0) && (nodes[i].m_numPaths != 0))
        {
            std::cout << "Node " << i << " can be reached but it's still incomplete. It might belong to some loop." << std::endl;
            queue[queueTop++] = i;
            nodes[i].m_pending = 0;
        }
    }

    initialNodePending = nodes[initialNode].m_pending;

    // Let's do BFS flood fill ignoring any possible loop.
    for (size_t currentIndex = 0; currentIndex < queueTop; ++currentIndex)
    {
        const Node&     current = nodes[queue[currentIndex]];
        std::cout << "Flood filling from node " << queue[currentIndex] << std::endl;
        for (NodeIndex linkIndex = current.m_firstLink; linkIndex < current.m_lastLink; ++linkIndex)
        {
            NodeIndex   childIndex = childLinks[linkIndex];
            Node&       child = nodes[childIndex];
            if (child.m_pending != 0)
            {
                std::cout << "  Next node is " << childIndex << std::endl;
                queue[queueTop++] = childIndex;
                child.m_pending = 0;
            }
        }
    }

    // See if the final node has been reached after flood-filling through all loops.
    if (nodes[finalNode].m_pending == 0)
    {
        // Yes, the final node is behind some loop(s).
        throw std::runtime_error("There are loops, and they stand in the way!");
    }

    if (initialNodePending != nodes[initialNode].m_pending)
    {
        throw std::runtime_error("Initial node has been visited again. It's part of some loop-2!");
    }

    // There are loops, but they don't affect the result.
    return nodes[finalNode].m_numPaths;
}


// -------------------------------------------------------------
//              Example
// -------------------------------------------------------------
Graph
InitGraph()
{
    using Edges = Graph::Edges;

    NodeIndex   numNodes = 10;
    Edges       edges;

    edges.push_back({0, 1});
    edges.push_back({0, 2});
    edges.push_back({0, 3});

    edges.push_back({1, 8});

    edges.push_back({3, 4});
    edges.push_back({3, 5});

    edges.push_back({4, 6});
    edges.push_back({5, 6});

    edges.push_back({2, 7});
    edges.push_back({6, 7});

    edges.push_back({7, 8});

    edges.push_back({7, 9});
    edges.push_back({8, 9});

// Loop!
//    edges.push_back({8, 2});

    return Graph(numNodes, edges);
}

int main()
{
    Graph       graph = InitGraph();
    NodeIndex   initialNode = 0;
    NodeIndex   finalNode = 9;

    try
    {
        BigInt      numPaths = CountPaths(graph, initialNode, finalNode);
        std::cout << "Number of paths from " << initialNode << " to " << finalNode << " = " << numPaths << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << e.what() << std::endl;
    }

    return 0;
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM