[英]Finding all cycles in a directed graph
如何在有向圖中從/到給定節點找到(迭代)所有循環?
例如,我想要這樣的東西:
A->B->A
A->B->C->A
但不是:B->C->B
我在搜索中找到了這個頁面,由於循環與強連通分量不同,我一直在搜索,最后,我找到了一個有效的算法,它列出了有向圖的所有(基本)循環。 它來自 Donald B. Johnson,該論文可以在以下鏈接中找到:
http://www.cs.tufts.edu/comp/150GA/homeworks/hw1/Johnson%2075.PDF
可以在以下位置找到 Java 實現:
http://normalisiert.de/code/java/elementaryCycles.zip
可以在此處找到 Johnson 算法的Mathematica演示,可以從右側下載實現( “下載作者代碼” )。
注意:實際上,這個問題有很多算法。 其中一些列在本文中:
http://dx.doi.org/10.1137/0205007
根據文章,約翰遜的算法是最快的。
帶有回溯的深度優先搜索應該在這里工作。 保留一組布爾值以跟蹤您之前是否訪問過節點。 如果您用完了要訪問的新節點(沒有碰到您已經訪問過的節點),那么只需回溯並嘗試不同的分支。
如果您有一個鄰接表來表示圖形,則 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)
首先 - 你真的不想嘗試找到所有的循環,因為如果有 1 那么有無數個循環。 例如 ABA、ABABA 等。或者有可能將 2 個循環合並成 8 個類似的循環等等……有意義的方法是尋找所有所謂的簡單循環——那些不交叉的循環,除了在起點/終點。 然后,如果您願意,您可以生成簡單循環的組合。
在有向圖中查找所有簡單循環的基線算法之一是:對圖中的所有簡單路徑(那些不相交的路徑)進行深度優先遍歷。 每次當當前節點在堆棧上有一個后繼節點時,就會發現一個簡單的循環。 它由堆棧上的元素組成,從確定的后繼者開始,到堆棧的頂部結束。 所有簡單路徑的深度優先遍歷類似於深度優先搜索,但除了當前在堆棧上的節點之外,您不會將訪問過的節點標記/記錄為停止點。
上面的蠻力算法效率非常低,此外還會生成多個循環副本。 然而,它是應用各種增強功能以提高性能和避免循環重復的多種實用算法的起點。 前段時間我驚訝地發現這些算法在教科書和網絡上並不容易獲得。 所以我做了一些研究,並在一個開源 Java 庫中實現了 4 種這樣的算法和 1 種算法,用於無向圖中的循環: http : //code.google.com/p/niographs/ 。
順便說一句,因為我提到了無向圖:那些算法是不同的。 構建一棵生成樹,然后每條不屬於樹的邊與樹中的一些邊一起形成一個簡單的循環。 以這種方式找到的循環形成所謂的循環基礎。 然后可以通過組合 2 個或更多不同的基本循環來找到所有簡單循環。 有關更多詳細信息,請參見例如: http : //dspace.mit.edu/bitstream/handle/1721.1/68106/FTL_R_1982_07.pdf 。
我發現解決這個問題的最簡單的選擇是使用名為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']]
澄清:
強連通分量會找到所有至少有一個環的子圖,而不是圖中所有可能的環。 例如,如果您將所有強連接組件折疊/分組/合並到一個節點中(即每個組件一個節點),您將得到一棵沒有循環的樹(實際上是一個 DAG)。 每個組件(基本上是一個包含至少一個循環的子圖)內部可以包含更多可能的循環,因此 SCC 不會找到所有可能的循環,它會找到所有可能的至少有一個循環的組,如果你分組它們,那么圖形將沒有循環。
要在圖中找到所有簡單的循環,正如其他人提到的,約翰遜算法是一個候選算法。
具有后邊緣的基於 DFS 的變體確實會找到循環,但在許多情況下,它不會是最小循環。 一般來說,DFS 會給你一個標志,表明存在一個循環,但它不足以實際找到循環。 例如,想象 5 個不同的循環共享兩條邊。 僅使用 DFS(包括回溯變體)沒有簡單的方法來識別周期。
Johnson 算法確實給出了所有唯一的簡單循環,並且具有良好的時間和空間復雜度。
但是,如果您只想找到最小循環(意味着可能有多個循環通過任何頂點並且我們有興趣找到最小循環)並且您的圖形不是很大,您可以嘗試使用下面的簡單方法。 與約翰遜的相比,它非常簡單但相當慢。
因此,找到 MINIMAL 循環的絕對最簡單的方法之一是使用 Floyd 算法使用鄰接矩陣找到所有頂點之間的最小路徑。 該算法遠不及 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
}
}
}
最初,該算法在加權邊圖上運行以查找所有節點對之間的所有最短路徑(因此是權重參數)。 為了使其正常工作,如果節點之間存在有向邊,則需要提供 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
有一次我被問到這是一個面試問題,我懷疑這發生在你身上,你是來這里尋求幫助的。 把問題分成三個問題,它變得更容易了。
問題 1) 使用迭代器模式提供一種迭代路由結果的方法。 放置獲取下一條路線的邏輯的好地方可能是迭代器的“moveNext”。 要找到有效的路線,這取決於您的數據結構。 對我來說,這是一個充滿有效路線可能性的 sql 表,所以我必須構建一個查詢來獲取給定源的有效目的地。
問題 2)當您找到每個節點時,將它們推入一個集合中,這意味着您可以通過查詢您正在構建的集合來非常輕松地查看您是否在一個點上“加倍”。
問題 3) 如果在任何時候你看到你正在加倍,你可以從集合中彈出一些東西並“備份”。 然后從那時起再次嘗試“前進”。
Hack:如果您使用的是 Sql Server 2008,那么如果您在樹中構建數據,則可以使用一些新的“層次結構”來快速解決這個問題。
在無向圖的情況下,最近發表的一篇論文(無向圖中的循環和 st 路徑的最優列表)提供了漸近最優解。 你可以在這里閱讀它http://arxiv.org/abs/1205.2766或在這里http://dl.acm.org/citation.cfm?id=2627951我知道它沒有回答你的問題,但由於標題你的問題沒有提到方向,它可能對谷歌搜索仍然有用
如果您想要在圖中找到所有基本電路,您可以使用自 1970 年以來在論文中發現的 JAMES C. TIERNA 的 EC 算法。
我設法在 php 中實現的非常原始的EC 算法(希望沒有錯誤如下所示)。 如果有循環,它也可以找到循環。 此實現中的電路(嘗試克隆原始電路)是非零元素。 零在這里代表不存在(我們知道為空)。
除了下面的其他實現,它使算法更加獨立,這意味着節點可以從任何地方開始,甚至可以從負數開始,例如 -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 進行了分析和記錄,但不幸的是,文檔是希臘語。
在 DAG 中查找所有循環涉及兩個步驟(算法)。
第一步是使用 Tarjan 算法找到強連通分量的集合。
第二步是在連接的組件中找到循環(路徑)。 我的建議是使用 Hierholzer 算法的修改版本。
這個想法是:
這是帶有測試用例的 Java 實現的鏈接:
http://stones333.blogspot.com/2013/12/find-cycles-in-directed-graph-dag.html
從節點 X 開始並檢查所有子節點(如果無向,父節點和子節點是等效的)。 將這些子節點標記為 X 的子節點。從任何此類子節點 A 中,將其標記為 A 的子節點 X',其中 X' 標記為距離 2 步。)。 如果您稍后點擊 X 並將其標記為 X'' 的子節點,則意味着 X 處於 3 節點循環中。 回溯到它的父級很容易(按原樣,該算法不支持這一點,因此您會找到具有 X' 的父級)。
注意:如果圖是無向圖或有任何雙向邊,則此算法會變得更加復雜,假設您不想在一個循環中兩次遍歷同一條邊。
我偶然發現了以下算法,它似乎比約翰遜的算法更有效(至少對於較大的圖形)。 然而,與 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();
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.