如何从有向图中找到(迭代)有向图中的所有周期?
例如,我想要这样的事情:
A->B->A
A->B->C->A
但不是: B-> C->乙
答案 0 :(得分:98)
我在搜索中找到了这个页面,因为周期与强连接组件不同,我继续搜索,最后,我找到了一个有效的算法,列出了有向图的所有(基本)周期。它来自Donald B. Johnson,论文可以在以下链接中找到:
http://www.cs.tufts.edu/comp/150GA/homeworks/hw1/Johnson%2075.PDF
可以在以下位置找到java实现:
http://normalisiert.de/code/java/elementaryCycles.zip
可以找到约翰逊算法的 Mathematica 演示here,可以从右侧("Download author code")下载实现。
注意:实际上,这个问题有很多算法。其中一些列在本文中:
http://dx.doi.org/10.1137/0205007
根据文章,约翰逊的算法是最快的算法。
答案 1 :(得分:30)
使用回溯的深度优先搜索应该在这里工作。 保留一个布尔值数组,以跟踪您之前是否访问过某个节点。如果你的新节点用完了(没有点击你已经存在过的节点),那么只需回溯并尝试不同的分支。
如果您有一个用于表示图形的邻接列表,则可以轻松实现DFS。例如,adj [A] = {B,C}表示B和C是A的子节点。
例如,下面的伪代码。 “start”是您开始的节点。
dfs(adj,node,visited):
if (visited[node]):
if (node == start):
"found a path"
return;
visited[node]=YES;
for child in adj[node]:
dfs(adj,child,visited)
visited[node]=NO;
使用起始节点调用上述函数:
visited = {}
dfs(adj,start,visited)
答案 2 :(得分:20)
首先 - 你真的不想尝试找到字面上的所有周期,因为如果有1那么就有无数个周期。例如ABA,ABABA等。或者可以将2个周期连接到8个周期等等......有意义的方法是寻找所有所谓的简单周期 - 那些不会跨越自己的周期在开始/结束点。然后,如果您希望您可以生成简单循环的组合。
在有向图中查找所有简单循环的基线算法之一是:在图中对所有简单路径(那些不自交的路径)进行深度优先遍历。每当当前节点在堆栈上具有后继节点时,就会发现一个简单的循环。它由堆栈中的元素组成,从标识的后继开始到堆栈顶部结束。所有简单路径的深度优先遍历类似于深度优先搜索,但您不会将当前在堆栈上的访问节点标记/记录为停靠点。
上面的暴力算法非常低效,除此之外还会生成多个循环副本。然而,它是多种实用算法的起点,它们应用各种增强功能以提高性能并避免循环重复。前一段时间我很惊讶地发现这些算法在教科书和网络上都不容易获得。所以我做了一些研究,并在开源Java库中的无向图中为循环实现了4个这样的算法和1个算法:http://code.google.com/p/niographs/。
BTW,因为我提到了无向图:它们的算法不同。构建生成树,然后不是树的一部分的每个边与树中的一些边形成一个简单的循环。发现这种循环形成了一个所谓的循环基础。然后可以通过组合2个或更多个不同的碱基循环来找到所有简单循环。有关详细信息,请参阅这个:http://dspace.mit.edu/bitstream/handle/1721.1/68106/FTL_R_1982_07.pdf。答案 3 :(得分:15)
我找到解决此问题的最简单选择是使用名为networkx
的python库。
它实现了在这个问题的最佳答案中提到的约翰逊算法,但它的执行非常简单。
简而言之,您需要以下内容:
import networkx as nx
import matplotlib.pyplot as plt
# Create Directed Graph
G=nx.DiGraph()
# Add a list of nodes:
G.add_nodes_from(["a","b","c","d","e"])
# Add a list of edges:
G.add_edges_from([("a","b"),("b","c"), ("c","a"), ("b","d"), ("d","e"), ("e","a")])
#Return a list of cycles described as a list o nodes
list(nx.simple_cycles(G))
答案: [['a','b','d','e'],['a','b','c']]
答案 4 :(得分:5)
澄清:
强连接组件将查找其中至少有一个周期的所有子图,而不是图中所有可能的周期。例如如果你采用所有强连接组件并将它们中的每一个折叠/分组/合并为一个节点(即每个组件的一个节点),你将获得一个没有循环的树(实际上是一个DAG)。每个组件(基本上是一个至少有一个循环的子图)可以在内部包含更多可能的循环,因此SCC将找不到所有可能的循环,它将找到至少有一个循环的所有可能组,如果您组他们,然后图表将没有周期。
在图表中找到所有简单循环,正如其他人所提到的,Johnson的算法是候选者。
答案 5 :(得分:3)
我曾经把这个作为面试问题给了我一次,我怀疑这件事已经发生在你身上,而你是来这里寻求帮助的。将问题分解为三个问题,变得更容易。
问题1) 使用迭代器模式提供迭代路由结果的方法。放置逻辑以获得下一个路径的好地方可能是迭代器的“moveNext”。要查找有效路由,它取决于您的数据结构。对我来说这是一个充满有效路由可能性的sql表,所以我必须构建一个查询来获取给定源的有效目的地。
问题2) 将每个节点推送到一个集合中时将它们推送到一个集合中,这意味着您可以通过查询正在构建的集合来查看是否在一个点上“双后”。
问题3) 如果在任何时候你看到你正在翻倍,你可以从收藏中弹出一些东西并“备份”。然后从那一点开始尝试再次“前进”。
Hack:如果您使用的是Sql Server 2008,那么如果您在树中构建数据,可以使用一些新的“层次结构”内容来快速解决此问题。
答案 6 :(得分:2)
在无向图的情况下,最近发表的一篇论文(无向图中的循环和st路径的最佳列表)提供了渐近最优解。您可以在此处http://arxiv.org/abs/1205.2766或此处http://dl.acm.org/citation.cfm?id=2627951阅读 我知道它没有回答你的问题,但由于你的问题的标题没有提到方向,它可能对Google搜索仍然有用
答案 7 :(得分:1)
在DAG中查找所有循环涉及两个步骤(算法)。
第一步是使用Tarjan的算法来查找一组强连接组件。
第二步是在连接的组件中找到循环(路径)。我的建议是使用Hierholzer算法的修改版本。
这个想法是:
以下是带有测试用例的Java实现的链接:
http://stones333.blogspot.com/2013/12/find-cycles-in-directed-graph-dag.html
答案 8 :(得分:1)
具有后沿的基于DFS的变体确实会找到周期,但在许多情况下,它不会是最小周期。一般来说,DFS会给你一个标志,表示有一个循环,但实际上找不到循环是不够的。例如,想象共享两条边的5个不同周期。没有简单的方法来识别仅使用DFS的循环(包括回溯变体)。
Johnson的算法确实提供了所有独特的简单周期,并且具有良好的时间和空间复杂性。
但是如果你想找到MINIMAL循环(意味着可能有多个循环通过任何顶点,我们有兴趣找到最小的循环)并且你的图形不是很大,你可以尝试使用简单的方法如下。 与约翰逊相比,它非常简单但相当缓慢。
因此,找到MINIMAL循环的绝对最简单的方法之一是使用Floyd的算法来查找使用邻接矩阵的所有顶点之间的最小路径。 这个算法远不及约翰逊那么理想,但它非常简单,内部循环非常紧密,对于较小的图形(<= 50-100个节点),使用它绝对有意义。 如果使用父跟踪,时间复杂度为O(n ^ 3),空间复杂度为O(n ^ 2),如果不使用,则为O(1)。 首先,让我们找到问题的答案,如果有一个循环。 该算法非常简单。下面是Scala的片段。
val NO_EDGE = Integer.MAX_VALUE / 2
def shortestPath(weights: Array[Array[Int]]) = {
for (k <- weights.indices;
i <- weights.indices;
j <- weights.indices) {
val throughK = weights(i)(k) + weights(k)(j)
if (throughK < weights(i)(j)) {
weights(i)(j) = throughK
}
}
}
最初,该算法在加权边缘图上运行,以找到所有节点对之间的所有最短路径(因此权重参数)。为了使其正常工作,如果节点之间存在有向边缘,则需要提供1,否则提供NO_EDGE。 算法执行后,您可以检查主对角线,如果有少于NO_EDGE的值,则该节点参与长度等于该值的循环。同一周期的每个其他节点将具有相同的值(在主对角线上)。
要重建循环本身,我们需要使用略微修改的算法版本与父跟踪。
def shortestPath(weights: Array[Array[Int]], parents: Array[Array[Int]]) = {
for (k <- weights.indices;
i <- weights.indices;
j <- weights.indices) {
val throughK = weights(i)(k) + weights(k)(j)
if (throughK < weights(i)(j)) {
parents(i)(j) = k
weights(i)(j) = throughK
}
}
}
如果顶点之间存在边缘,则父节点矩阵最初应包含边缘单元格中的源顶点索引,否则为-1。 函数返回后,对于每个边,您将引用最短路径树中的父节点。 然后很容易恢复实际周期。
总而言之,我们有以下程序来查找所有最小周期
val NO_EDGE = Integer.MAX_VALUE / 2;
def shortestPathWithParentTracking(
weights: Array[Array[Int]],
parents: Array[Array[Int]]) = {
for (k <- weights.indices;
i <- weights.indices;
j <- weights.indices) {
val throughK = weights(i)(k) + weights(k)(j)
if (throughK < weights(i)(j)) {
parents(i)(j) = parents(i)(k)
weights(i)(j) = throughK
}
}
}
def recoverCycles(
cycleNodes: Seq[Int],
parents: Array[Array[Int]]): Set[Seq[Int]] = {
val res = new mutable.HashSet[Seq[Int]]()
for (node <- cycleNodes) {
var cycle = new mutable.ArrayBuffer[Int]()
cycle += node
var other = parents(node)(node)
do {
cycle += other
other = parents(other)(node)
} while(other != node)
res += cycle.sorted
}
res.toSet
}
和一个小的主要方法来测试结果
def main(args: Array[String]): Unit = {
val n = 3
val weights = Array(Array(NO_EDGE, 1, NO_EDGE), Array(NO_EDGE, NO_EDGE, 1), Array(1, NO_EDGE, NO_EDGE))
val parents = Array(Array(-1, 1, -1), Array(-1, -1, 2), Array(0, -1, -1))
shortestPathWithParentTracking(weights, parents)
val cycleNodes = parents.indices.filter(i => parents(i)(i) < NO_EDGE)
val cycles: Set[Seq[Int]] = recoverCycles(cycleNodes, parents)
println("The following minimal cycle found:")
cycles.foreach(c => println(c.mkString))
println(s"Total: ${cycles.size} cycle found")
}
,输出
The following minimal cycle found:
012
Total: 1 cycle found
答案 9 :(得分:1)
从节点X开始并检查所有子节点(如果未定向,父节点和子节点是等效的)。将这些子节点标记为X的子节点。从任何此类子节点A,将其标记为A,X'的子节点,其中X'标记为2步之外。)。如果你以后点击X并将其标记为X''的子节点,则表示X处于3节点循环中。回溯它的父亲是很容易的(因为,算法不支持这个,所以你找到任何父有X')。
注意:如果图形是无向的或具有任何双向边缘,则该算法会变得更复杂,假设您不想在一个周期内遍历相同的边缘两次。
答案 10 :(得分:1)
如果您想要的是在图表中找到所有基本电路,您可以使用自1970年以来在纸上找到的EC C.算法。
非常原始的 EC算法,因为我设法在php中实现它(希望下面没有错误)。如果有的话,它也可以找到循环。此实现中的电路(试图克隆原始电路)是非零元素。零在这里代表不存在(我们知道它是null)。
除了以下内容之外,还有另一种实现方法可以使算法更加独立,这意味着节点可以从任何地方开始,甚至可以从负数开始,例如-4,-3,-2,等等。
在这两种情况下,都要求节点是顺序的。
您可能需要学习原始论文James C. Tiernan Elementary Circuit Algorithm
<?php
echo "<pre><br><br>";
$G = array(
1=>array(1,2,3),
2=>array(1,2,3),
3=>array(1,2,3)
);
define('N',key(array_slice($G, -1, 1, true)));
$P = array(1=>0,2=>0,3=>0,4=>0,5=>0);
$H = array(1=>$P, 2=>$P, 3=>$P, 4=>$P, 5=>$P );
$k = 1;
$P[$k] = key($G);
$Circ = array();
#[Path Extension]
EC2_Path_Extension:
foreach($G[$P[$k]] as $j => $child ){
if( $child>$P[1] and in_array($child, $P)===false and in_array($child, $H[$P[$k]])===false ){
$k++;
$P[$k] = $child;
goto EC2_Path_Extension;
} }
#[EC3 Circuit Confirmation]
if( in_array($P[1], $G[$P[$k]])===true ){//if PATH[1] is not child of PATH[current] then don't have a cycle
$Circ[] = $P;
}
#[EC4 Vertex Closure]
if($k===1){
goto EC5_Advance_Initial_Vertex;
}
//afou den ksana theoreitai einai asfales na svisoume
for( $m=1; $m<=N; $m++){//H[P[k], m] <- O, m = 1, 2, . . . , N
if( $H[$P[$k-1]][$m]===0 ){
$H[$P[$k-1]][$m]=$P[$k];
break(1);
}
}
for( $m=1; $m<=N; $m++ ){//H[P[k], m] <- O, m = 1, 2, . . . , N
$H[$P[$k]][$m]=0;
}
$P[$k]=0;
$k--;
goto EC2_Path_Extension;
#[EC5 Advance Initial Vertex]
EC5_Advance_Initial_Vertex:
if($P[1] === N){
goto EC6_Terminate;
}
$P[1]++;
$k=1;
$H=array(
1=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
2=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
3=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
4=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
5=>array(1=>0,2=>0,3=>0,4=>0,5=>0)
);
goto EC2_Path_Extension;
#[EC5 Advance Initial Vertex]
EC6_Terminate:
print_r($Circ);
?>
然后这是另一个实现,更独立于图形,没有goto和没有数组值,而是使用数组键,路径,图形和电路存储为数组键(如果你愿意,使用数组值,只是改变所需的线条)。示例图从-4开始显示其独立性。
<?php
$G = array(
-4=>array(-4=>true,-3=>true,-2=>true),
-3=>array(-4=>true,-3=>true,-2=>true),
-2=>array(-4=>true,-3=>true,-2=>true)
);
$C = array();
EC($G,$C);
echo "<pre>";
print_r($C);
function EC($G, &$C){
$CNST_not_closed = false; // this flag indicates no closure
$CNST_closed = true; // this flag indicates closure
// define the state where there is no closures for some node
$tmp_first_node = key($G); // first node = first key
$tmp_last_node = $tmp_first_node-1+count($G); // last node = last key
$CNST_closure_reset = array();
for($k=$tmp_first_node; $k<=$tmp_last_node; $k++){
$CNST_closure_reset[$k] = $CNST_not_closed;
}
// define the state where there is no closure for all nodes
for($k=$tmp_first_node; $k<=$tmp_last_node; $k++){
$H[$k] = $CNST_closure_reset; // Key in the closure arrays represent nodes
}
unset($tmp_first_node);
unset($tmp_last_node);
# Start algorithm
foreach($G as $init_node => $children){#[Jump to initial node set]
#[Initial Node Set]
$P = array(); // declare at starup, remove the old $init_node from path on loop
$P[$init_node]=true; // the first key in P is always the new initial node
$k=$init_node; // update the current node
// On loop H[old_init_node] is not cleared cause is never checked again
do{#Path 1,3,7,4 jump here to extend father 7
do{#Path from 1,3,8,5 became 2,4,8,5,6 jump here to extend child 6
$new_expansion = false;
foreach( $G[$k] as $child => $foo ){#Consider each child of 7 or 6
if( $child>$init_node and isset($P[$child])===false and $H[$k][$child]===$CNST_not_closed ){
$P[$child]=true; // add this child to the path
$k = $child; // update the current node
$new_expansion=true;// set the flag for expanding the child of k
break(1); // we are done, one child at a time
} } }while(($new_expansion===true));// Do while a new child has been added to the path
# If the first node is child of the last we have a circuit
if( isset($G[$k][$init_node])===true ){
$C[] = $P; // Leaving this out of closure will catch loops to
}
# Closure
if($k>$init_node){ //if k>init_node then alwaya count(P)>1, so proceed to closure
$new_expansion=true; // $new_expansion is never true, set true to expand father of k
unset($P[$k]); // remove k from path
end($P); $k_father = key($P); // get father of k
$H[$k_father][$k]=$CNST_closed; // mark k as closed
$H[$k] = $CNST_closure_reset; // reset k closure
$k = $k_father; // update k
} } while($new_expansion===true);//if we don't wnter the if block m has the old k$k_father_old = $k;
// Advance Initial Vertex Context
}//foreach initial
}//function
?>
我已经对EC进行了分析和记录,但遗憾的是文档是希腊语。
答案 11 :(得分:0)
使用不相交集链接列表的Javascript解决方案。可以升级到不相交的集合林,以加快运行时间。
var input = '5\nYYNNN\nYYYNN\nNYYNN\nNNNYN\nNNNNY'
console.log(input);
//above solution should be 3 because the components are
//{0,1,2}, because {0,1} and {1,2} therefore {0,1,2}
//{3}
//{4}
//MIT license, authored by Ling Qing Meng
//'4\nYYNN\nYYYN\nNYYN\nNNNY'
//Read Input, preformatting
var reformat = input.split(/\n/);
var N = reformat[0];
var adjMatrix = [];
for (var i = 1; i < reformat.length; i++) {
adjMatrix.push(reformat[i]);
}
//for (each person x from 1 to N) CREATE-SET(x)
var sets = [];
for (var i = 0; i < N; i++) {
var s = new LinkedList();
s.add(i);
sets.push(s);
}
//populate friend potentials using combinatorics, then filters
var people = [];
var friends = [];
for (var i = 0; i < N; i++) {
people.push(i);
}
var potentialFriends = k_combinations(people,2);
for (var i = 0; i < potentialFriends.length; i++){
if (isFriend(adjMatrix,potentialFriends[i]) === 'Y'){
friends.push(potentialFriends[i]);
}
}
//for (each pair of friends (x y) ) if (FIND-SET(x) != FIND-SET(y)) MERGE-SETS(x, y)
for (var i = 0; i < friends.length; i++) {
var x = friends[i][0];
var y = friends[i][1];
if (FindSet(x) != FindSet(y)) {
sets.push(MergeSet(x,y));
}
}
for (var i = 0; i < sets.length; i++) {
//sets[i].traverse();
}
console.log('How many distinct connected components?',sets.length);
//Linked List data structures neccesary for above to work
function Node(){
this.data = null;
this.next = null;
}
function LinkedList(){
this.head = null;
this.tail = null;
this.size = 0;
// Add node to the end
this.add = function(data){
var node = new Node();
node.data = data;
if (this.head == null){
this.head = node;
this.tail = node;
} else {
this.tail.next = node;
this.tail = node;
}
this.size++;
};
this.contains = function(data) {
if (this.head.data === data)
return this;
var next = this.head.next;
while (next !== null) {
if (next.data === data) {
return this;
}
next = next.next;
}
return null;
};
this.traverse = function() {
var current = this.head;
var toPrint = '';
while (current !== null) {
//callback.call(this, current); put callback as an argument to top function
toPrint += current.data.toString() + ' ';
current = current.next;
}
console.log('list data: ',toPrint);
}
this.merge = function(list) {
var current = this.head;
var next = current.next;
while (next !== null) {
current = next;
next = next.next;
}
current.next = list.head;
this.size += list.size;
return this;
};
this.reverse = function() {
if (this.head == null)
return;
if (this.head.next == null)
return;
var currentNode = this.head;
var nextNode = this.head.next;
var prevNode = this.head;
this.head.next = null;
while (nextNode != null) {
currentNode = nextNode;
nextNode = currentNode.next;
currentNode.next = prevNode;
prevNode = currentNode;
}
this.head = currentNode;
return this;
}
}
/**
* GENERAL HELPER FUNCTIONS
*/
function FindSet(x) {
for (var i = 0; i < sets.length; i++){
if (sets[i].contains(x) != null) {
return sets[i].contains(x);
}
}
return null;
}
function MergeSet(x,y) {
var listA,listB;
for (var i = 0; i < sets.length; i++){
if (sets[i].contains(x) != null) {
listA = sets[i].contains(x);
sets.splice(i,1);
}
}
for (var i = 0; i < sets.length; i++) {
if (sets[i].contains(y) != null) {
listB = sets[i].contains(y);
sets.splice(i,1);
}
}
var res = MergeLists(listA,listB);
return res;
}
function MergeLists(listA, listB) {
var listC = new LinkedList();
listA.merge(listB);
listC = listA;
return listC;
}
//access matrix by i,j -> returns 'Y' or 'N'
function isFriend(matrix, pair){
return matrix[pair[0]].charAt(pair[1]);
}
function k_combinations(set, k) {
var i, j, combs, head, tailcombs;
if (k > set.length || k <= 0) {
return [];
}
if (k == set.length) {
return [set];
}
if (k == 1) {
combs = [];
for (i = 0; i < set.length; i++) {
combs.push([set[i]]);
}
return combs;
}
// Assert {1 < k < set.length}
combs = [];
for (i = 0; i < set.length - k + 1; i++) {
head = set.slice(i, i+1);
tailcombs = k_combinations(set.slice(i + 1), k - 1);
for (j = 0; j < tailcombs.length; j++) {
combs.push(head.concat(tailcombs[j]));
}
}
return combs;
}
答案 12 :(得分:0)
来自起始节点的DFS,在遍历期间跟踪DFS路径,如果在s的路径中找到来自节点v的边,则记录路径。 (v,s)是DFS树中的后沿,因此表示包含s的循环。
答案 13 :(得分:0)
我偶然发现了以下算法,这种算法似乎比约翰逊的算法更有效(至少对于较大的图表而言)。然而,与Tarjan的算法相比,我不确定它的性能 另外,到目前为止我只检查了三角形。如果有兴趣,请参阅Norishige Chiba和Takao Nishizeki的“Arboricity and Subgraph Listing Algorithms”(http://dx.doi.org/10.1137/0214017)
答案 14 :(得分:0)
关于置换周期的问题,请在此处阅读更多内容: https://www.codechef.com/problems/PCYCLE
您可以尝试此代码(输入大小和位数):
# include<cstdio>
using namespace std;
int main()
{
int n;
scanf("%d",&n);
int num[1000];
int visited[1000]={0};
int vindex[2000];
for(int i=1;i<=n;i++)
scanf("%d",&num[i]);
int t_visited=0;
int cycles=0;
int start=0, index;
while(t_visited < n)
{
for(int i=1;i<=n;i++)
{
if(visited[i]==0)
{
vindex[start]=i;
visited[i]=1;
t_visited++;
index=start;
break;
}
}
while(true)
{
index++;
vindex[index]=num[vindex[index-1]];
if(vindex[index]==vindex[start])
break;
visited[vindex[index]]=1;
t_visited++;
}
vindex[++index]=0;
start=index+1;
cycles++;
}
printf("%d\n",cycles,vindex[0]);
for(int i=0;i<(n+2*cycles);i++)
{
if(vindex[i]==0)
printf("\n");
else
printf("%d ",vindex[i]);
}
}
答案 15 :(得分:0)
你不能做一个小的递归函数来遍历节点吗?
readDiGraph( string pathSoFar, Node x)
{
if(NoChildren) MasterList.add( pathsofar + Node.name ) ;
foreach( child )
{
readDiGraph( pathsofar + "->" + this.name, child)
}
}
如果您有大量节点,您将耗尽堆栈
答案 16 :(得分:0)
答案 17 :(得分:0)
DFS c ++版本的二楼答案中的伪代码:
void findCircleUnit(int start, int v, bool* visited, vector<int>& path) {
if(visited[v]) {
if(v == start) {
for(auto c : path)
cout << c << " ";
cout << endl;
return;
}
else
return;
}
visited[v] = true;
path.push_back(v);
for(auto i : G[v])
findCircleUnit(start, i, visited, path);
visited[v] = false;
path.pop_back();
}
答案 18 :(得分:0)