我如何找到(迭代)有向图中给定节点的所有循环?
例如,我想要这样的东西:
A->B->A
A->B->C->A
但不是:B-> C-> B
我如何找到(迭代)有向图中给定节点的所有循环?
例如,我想要这样的东西:
A->B->A
A->B->C->A
但不是:B-> C-> B
Answers:
我在搜索中找到了该页面,由于循环与强连接的组件不同,因此我一直进行搜索,最后,我找到了一种有效的算法,该算法列出了有向图的所有(基本)循环。它来自唐纳德·B·约翰逊(Donald B. Johnson),可以在以下链接中找到该论文:
http://www.cs.tufts.edu/comp/150GA/homeworks/hw1/Johnson%2075.PDF
Java实现可在以下位置找到:
http://normalisiert.de/code/java/elementaryCycles.zip
一个数学约翰逊的算法演示可以发现这里实现,可以从右边(下载“下载作者码”)。
注意:实际上,有很多算法可以解决此问题。本文中列出了其中一些:
http://dx.doi.org/10.1137/0205007
根据文章,约翰逊算法是最快的算法。
A->B->C->A
基本的吗?
simple_cycle
与networkx相同。
具有回溯功能的深度优先搜索应该在这里起作用。保留一个布尔值数组,以跟踪您之前是否访问过节点。如果您用完了要去的新节点(没有击中您已经去过的节点),则只需回溯并尝试其他分支。
如果您有一个邻接表来表示图,则DFS易于实现。例如adj [A] = {B,C}表示B和C是A的子代。
例如,下面的伪代码。“开始”是您从其开始的节点。
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)
if (node == start):
- node and start
首次通话是什么
start
)的所有循环。它从该顶点开始并执行DFS,直到再次回到该顶点,然后知道已找到一个循环。但是它实际上并没有输出周期,只是输出了一些周期(但是修改它来做到这一点应该不会太困难)。
start
。您不必真正清除已访问的标志,因为每个已访问的标志都会由于清除visited[node]=NO;
。但是请记住,如果您有一个周期A->B->C->A
,您将检测到3次,就像其中start
任何3 次一样。防止出现这种情况的一个方法是,要设置另一个访问数组,在该数组中start
设置在某个时刻成为节点的每个节点,然后就不必重新访问它们了。
首先-您真的不想尝试从字面上看所有周期,因为如果有1,那么就有无限多个周期。例如ABA,ABABA等。或者也可以将2个循环连接成一个类似8的循环,以此类推...等等。有意义的方法是寻找所有所谓的简单循环-除自身以外不交叉的循环在起点/终点。然后,如果您愿意,可以生成简单循环的组合。
在有向图中查找所有简单循环的基线算法之一是:对图中的所有简单路径(不交叉的路径)进行深度优先遍历。每当当前节点在堆栈上具有后继者时,都会发现一个简单的周期。它由堆栈中的元素组成,这些元素从已标识的后继者开始,到堆栈顶部为止。所有简单路径的深度优先遍历与深度优先搜索相似,但是除了当前堆栈中的那些节点以外,您不会标记/记录已访问的节点作为停止点。
上面的蛮力算法效率极低,此外还产生多个循环副本。然而,这是多种实用算法的起点,这些实用算法应用了各种增强功能以提高性能并避免循环重复。令我惊讶的是,不久前发现教科书和网络上还没有这些算法。因此,我进行了一些研究,并在http://code.google.com/p/niographs/的开放源Java库中实现了4种此类算法和1种针对无向图循环的算法。
顺便说一句,因为我提到了无向图:那些算法是不同的。生成一棵生成树,然后不属于该树的每个边缘与该树中的某些边缘一起形成一个简单的循环。以这种方式发现的循环形成了所谓的循环基础。然后可以通过组合2个或更多个不同的基本循环来找到所有简单循环。有关更多详细信息,请参见:http : //dspace.mit.edu/bitstream/handle/1721.1/68106/FTL_R_1982_07.pdf。
jgrapht
所使用的http://code.google.com/p/niographs/
示例可以从github.com/jgrapht/jgrapht/wiki/DirectedGraphDemo中
我发现解决此问题的最简单选择是使用名为的python库networkx
。
它实现了该问题的最佳答案中提到的约翰逊算法,但是执行起来非常简单。
简而言之,您需要以下内容:
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']]
nx.DiGraph({'a': ['b'], 'b': ['c','d'], 'c': ['a'], 'd': ['e'], 'e':['a']})
曾经有人问我这是面试问题,我怀疑这已经发生在您身上,您正在这里寻求帮助。将问题分为三个问题,将变得更加容易。
问题1)使用迭代器模式提供一种迭代路由结果的方法。放置逻辑以获取下一条路线的好地方可能是迭代器的“ moveNext”。要找到有效的路由,这取决于您的数据结构。对我来说,这是一个充满有效路由可能性的sql表,因此我必须构建查询以获取给定源的有效目的地。
问题2)将找到的每个节点推入到集合中,这意味着您可以通过查询正在构建的集合来非常轻松地查看是否在某个点上“加倍返回”。
问题3)如果在任何时候看到您要加倍,您可以从集合中弹出内容并“备份”。然后从这一点开始尝试再次“前进”。
哈克:如果您使用的是Sql Server 2008,则可以使用树中的数据来构造一些新的“层次结构”,以快速解决此问题。
具有后边缘的基于DFS的变体确实可以找到循环,但是在许多情况下,它不是最小的循环。通常,DFS会为您提供一个标记,表明存在一个循环,但不足以实际找到循环。例如,假设5个不同的周期共享两个边。没有简单的方法仅使用DFS(包括回溯变体)来识别周期。
Johnson的算法确实给出了所有独特的简单循环,并且具有良好的时间和空间复杂性。
但是,如果您只想找到最小周期(意味着可能有一个以上的周期遍历任何顶点,而我们有兴趣寻找最小顶点)并且您的图形不是很大,则可以尝试使用以下简单方法。与Johnson's相比,它非常简单,但速度却很慢。
因此,找到最小周期的绝对最简单的方法之一是使用弗洛伊德算法使用邻接矩阵在所有顶点之间找到最小路径。该算法远不及Johnson算法最佳,但它是如此简单,并且其内部循环非常紧密,以至于对于较小的图形(<= 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
}
}
}
最初,该算法在加权边图上运行,以找到所有节点对之间的所有最短路径(因此,使用weights参数)。为了使其正常工作,如果节点之间有定向边,则需要提供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
对于无向图,最近发表的一篇论文(无向图中的循环和st路径的最佳列表)提供了一种渐近最优解。您可以在http://arxiv.org/abs/1205.2766或http://dl.acm.org/citation.cfm?id=2627951上阅读它, 我知道它不能回答您的问题,但是由于您的问题没有提到方向,它可能仍对Google搜索有用
如果要在图形中查找所有基本电路,可以使用JAMES C. TIERNAN的EC算法,该算法自1970年以来在纸上发现。
在非常原始的 EC算法我设法实现它在PHP(希望没有错误如下图所示)。如果有循环,它也可以找到循环。此实现中的电路(试图克隆原始电路)是非零元素。这里的零代表不存在(我们知道它为null)。
除了下面的描述之外,还有另一个实现使算法更加独立,它意味着节点可以从任何地方开始,甚至可以从负数开始,例如-4,-3,-2等。
在这两种情况下,都要求节点是顺序的。
您可能需要研究原始论文James C. Tiernan基本电路算法
<?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,但是很遗憾,该文档使用的是希腊文。
在DAG中查找所有循环涉及两个步骤(算法)。
第一步是使用Tarjan的算法来查找一组强连接的组件。
第二步是在连接的组件中查找循环(路径)。我的建议是使用Hierholzer算法的修改版本。
这个想法是:
这是带有测试用例的Java实现的链接:
http://stones333.blogspot.com/2013/12/find-cycles-in-directed-graph-dag.html
我偶然发现了以下算法,该算法似乎比约翰逊算法更有效(至少对于较大的图形而言)。但是,与Tarjan的算法相比,我不确定其性能。
此外,到目前为止,我只检查了三角形。如果有兴趣,请参阅Norishige Chiba和Takao Nishizeki的“树状图和子图列出算法”(http://dx.doi.org/10.1137/0214017)
使用脱节集链接列表的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;
}
从起始节点s开始的DFS,在遍历期间跟踪DFS路径,如果在到s的路径中找到节点v的边缘,则记录该路径。(v,s)是DFS树中的后端,因此指示包含s的循环。
关于您有关置换周期的问题,请在此处阅读更多信息: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]);
}
}
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();
}