[英]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。
任务可以这样表述:
G
,由节点{Ni}
和边{Ej}
组成。G
可以有任何东西:循环、断开连接的组、孤立节点等。Ninitial
到节点Nfinal
的唯一轨迹的数量。由于其复杂性,应避免使用 DFS。 想想菱形图,它可能导致 DFS 的指数级复杂度:
N2 N5 N8
/ \ / \ / \ ....
-- N1 N4 N7 N10
\ / \ / \ / ....
N3 N6 N9
该算法的推理是:
Pi
为从节点Ninitial
到节点Ni
的轨迹数。Ej
的轨迹数。Ej
,其Lj
等于该边起始节点的Pi
。Lj
之和。Pinitial = 1
。算法本身是:
Ni
意味着将它的Pi
传播到它的所有出边。 结果,图中下方的一些直接相邻节点可能变得“完整”。就这样。
在非循环图中,上述算法将迭代所有节点并计算所有可达节点的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.