【问题标题】:Find all cycles in graph, redux查找图中的所有循环,redux
【发布时间】:2010-05-15 11:24:52
【问题描述】:

我知道这个问题有很多答案。但是,我发现他们都没有真正做到这一点。
有人认为,循环(几乎)与强连通分量(s.Finding all cycles in a directed graph)相同,因此可以使用为该目标设计的算法。
有人认为,可以通过 DFS 并检查后边(s. boost graph documentation on file dependencies)来找到 a 循环。

我现在想就是否可以通过 DFS 检测到图中的 所有 循环并检查后边提出一些建议?
http://www.me.utexas.edu/~bard/IP/Handouts/cycles.pdf(在 SO 上找到)表示一种基于方法在循环基础上。就我个人而言,我觉得它不是很直观,所以我正在寻找不同的解决方案。

编辑:我最初的看法显然是错误的。 S.下一个答案是“白痴”。
初步意见: 我的观点是,当 DFS-VISIT(DFS 的伪代码)新进入每个尚未访问的节点时,它确实可以这样工作。从这个意义上说,每个顶点都展示了一个循环的潜在开始。此外,由于 DFS 访问每条边一次,因此也覆盖了通向循环起点的每条边。因此,通过使用 DFS 和后边检查,确实应该可以检测到图中的 所有 循环。请注意,如果存在具有不同数量参与者节点的循环(例如三角形、矩形等),则必须做额外的工作来区分每个循环的实际“形状”。

【问题讨论】:

    标签: graph graph-theory depth-first-search triangle-count


    【解决方案1】:

    我已经彻底回答了这个问题,所以检查一下:

    Will a source-removal sort always return a maximal cycle?

    答案的相关部分:

    对您的 图表。

    您有兴趣识别回 边,即在遍历中,一条边 它指向一个祖先(在 DFS 树,由 第一个访问节点的边缘 访问节点的时间)。为了 例如,如果 DFS 堆栈有节点 [A->B->C->D] 当你探索 D 你找到一个边缘 D->B,那是一个背面 边缘。每个后沿定义一个循环。

    更重要的是,引起的循环 通过后边缘是一组基本的 图的循环。 “一套基本的 循环”:您可以构建所有循环 仅由 UNIONing 和 基本集的异或循环。为了 例如,考虑循环 [A1->A2->A3->A1] 和 [A2->B1->B2->B3->A2]。你可以联合 他们进入循环: [A1->A2->B1->B2->B3->A2->A3->A1].

    【讨论】:

      【解决方案2】:

      也许这可以以某种方式帮助您,我找到了this 站点,其中描述了用于有向图的彩色 dfs。所以你可以考虑把我这里介绍的 dfs 翻译成 php。

      我添加的是创建森林的部分和查找所有循环的其他部分。因此,请考虑将我的代码的这两部分确定为正确是不安全的。

      具有图论知识的人可能可以肯定地进行测试。 dfs 部分中没有 cmets,因为它已在参考站点中进行了描述。我建议你以不止一棵树为例,在纸上画出森林(需要 4 种颜色)以便更好地理解。

      这是代码:

       <?php 
      
          //define the graph
          $g = Array(
          1 => Array(1,2,3,4,5),
          2 => Array(1,2,3,4,5),
          3 => Array(1,2,3,4,5),
          4 => Array(1,2,3,4,5),
          5 => Array(1,2,3,4,5)
          );
      
          //add needed attributes on the graph
          $G = Array();
          foreach($g as $name => $children)
          {
              $child = Array();
              foreach($children as $child_name)//attaching a v letter is not needed, just a preference
                  $child['v'.$child_name] = null;
      
              $G['v'.$name] = 
              Array('child'=>$child, 
                  'color'=>'WHITE',//is used in dfs to make visit
                  'discover_time'=>null,//is used in dfs to classify edges
                  'finish_time'=>null,//is used in dfs to classify edges                  
                  'father'=>null,//is used to walk backward to the start of a cycle
                  'back_edge'=>null//can be omited, this information can be found with in_array(info, child_array)
              );
          }   
      
      
      new test($G);
      class test
      {
          private $G = Array();//graph
          private $C = Array();//cycles
          private $F = Array();//forest
          private $L = Array();//loops
          private $time_counter = 0;
      
          public function __construct($G)
          {
              $this->G = $G;
      
              foreach($this->G as $node_name => $foo)
              {
                  if($this->G[$node_name]['color'] === 'WHITE')
                      $this->DFS_Visit($node_name);
              }
      
              $tree =array();
              foreach($this->G as $node_name => $data)
              {
                  if($data['father'] === null)//new tree found
                  {
                      $this->F[] = $tree;//at first an empty array is inserted
                      $tree = Array();
                      $tree[$node_name] = $data; 
                  }
                  else
                      $tree[$node_name] = $data;
              }
              array_shift($this->F);//remove the empty array
              $this->F[] = $tree;//add the last tree
      
              $this->find_all_elementary_cycles();
      
              file_put_contents('dump.log', count($this->L)." Loops found: \n", FILE_APPEND);
              file_put_contents('dump.log', print_r($this->L,true)."\n", FILE_APPEND);
      
              file_put_contents('dump.log', count($this->C)." Cycles found: \n", FILE_APPEND);
              file_put_contents('dump.log', print_r($this->C,true)."\n", FILE_APPEND);
      
              file_put_contents('dump.log', count($this->F)." trees found in the Forest: \n", FILE_APPEND);
              file_put_contents('dump.log', print_r($this->F,true)."\n", FILE_APPEND);
          }
      
          public function find_all_elementary_cycles()
          {
              /*** For each tree of the forest ***/
              foreach($this->F as $tree)
              {
                  /*** Foreach node in the tree ***/
                  foreach($tree as $node_name => $node)
                  {
                      /*** If this tree node connects to some child with backedge 
                      (we hope to avoid some loops with this if) ***/
                      if ( $node['back_edge'] === true )
                          /*** Then for every child ***/
                          foreach ( $node['child'] as $child_name => $edge_classification)
                              if($edge_classification === 'BACK_EDGE')
                                  $this->back_edge_exploit($node_name, $child_name, $tree);               
                  }
              }
          }
      
          private function back_edge_exploit ($back_edge_sender, $back_edge_receiver, $tree)
          {
              /*** The input of this function is a back edge, a back edge is defined as follows
              -a sender node: which stands lower in the tree and a reciever node which of course stands higher
              ***/
      
              /*** We want to get rid of loops, so check for a loop ***/
              if($back_edge_sender == $back_edge_receiver)
                  return $this->L[] = $back_edge_sender;//we need to return cause no there is no cycle in a loop
      
              /*** For this backedge sender node the backedge reciever might send a direct edge to the sender ***/
              if( isset($this->G[$back_edge_receiver]['child'][$back_edge_sender]) > 0 )
                  /*** This edge that the reciever sends could be a tree edge, this will happen 
                  -in the case that: the backedge reciever is a father, but it can be a forward edge
                  -in this case: for the forward edge to exist the backedge reciever does not have to be 
                  -a father onlly: it can also be an ancestore. Whatever of both cases, we have a cycle
                  ***/
                  if( $this->G[$back_edge_receiver]['child'][$back_edge_sender] === 'TREE_EDGE' or 
                      $this->G[$back_edge_receiver]['child'][$back_edge_sender] === 'FORWARD_EDGE')
                          $this->C[md5(serialize(Array($back_edge_receiver,$back_edge_sender)))]
                          = Array($back_edge_receiver,$back_edge_sender);
      
      
              /*** Until now we have covered, loops, cycles of the kind father->child which occur from one tree edge 
              - and one: back edge combination, and also we have covered cycles of the kind ancestore->descendant 
              -which: occur from the combination of one forward edge and one backedge (of course might happen that 
              -a father: can send to the child both forward and tree edge, all these are covered already).
              -what are left: are the cycles of the combination of more than one tree edges and one backedge ***/
              $cycle = Array();
              attach_node://loops must be handled before this, otherwise goto will loop continously
              $cycle[] =  $back_edge_sender; //enter the backedge sender
              $back_edge_sender = $tree[$back_edge_sender]['father']; //backedge sender becomes his father
              if($back_edge_sender !== $back_edge_receiver) //if backedge sender has not become backedge reciever yet
                  goto attach_node;//the loop again
      
              $cycle[] = $back_edge_receiver;
              $cycle = array_reverse($cycle);
              $this->C[md5(serialize($cycle))] = $cycle;
          }
      
      
          private function DFS_Visit($node_name)
          { 
              $this->G[$node_name]['color'] = 'GRAY';
              $this->G[$node_name]['discover_time'] = $this->time_counter++;
      
              foreach($this->G[$node_name]['child'] as $child_name => $foo)
              {               
                      if($this->G[$child_name]['color'] === 'BLACK') {#child black 
      
                          if( $this->G[$node_name]['discover_time'] < 
                          $this->G[$child_name]['discover_time'] ){#time of father smaller
                              $this->G[$node_name]['child'][$child_name] = 'FORWARD_EDGE';
                          }
                          else{#time of child smaller
                              $this->G[$node_name]['child'][$child_name] = 'CROSS_EDGE';
                          }
                      }
                      elseif($this->G[$child_name]['color'] === 'WHITE'){#child white
                          $this->G[$node_name]['child'][$child_name] = 'TREE_EDGE';
                          $this->G[$child_name]['father'] = $node_name;#father in the tree
                          $this->DFS_Visit($child_name);
                      }#child discovered but not explored (and father discovered)
                      elseif($this->G[$child_name]['color'] === 'GRAY'){#child gray
                          $this->G[$node_name]['child'][$child_name] = 'BACK_EDGE';
                          $this->G[$node_name]['back_edge'] = true;
                      }
              }//for  
              $this->G[$node_name]['color'] = 'BLACK';//fully explored
              $this->G[$node_name]['finish_time'] = $this->time_counter++;
          }
      
      }
      
      ?>
      

      这是输出:

      5 Loops found:  Array (
          [0] => v1
          [1] => v2
          [2] => v3
          [3] => v4
          [4] => v5 )
      
      16 Cycles found:  Array (
          [336adbca89b3389a6f9640047d07f24a] => Array
              (
                  [0] => v1
                  [1] => v2
              )
      
          [d68df8cdbc98d846a591937e9dd9cd70] => Array
              (
                  [0] => v1
                  [1] => v3
              )
      
          [cad6b68c862d3a00a35670db31b76b67] => Array
              (
                  [0] => v1
                  [1] => v2
                  [2] => v3
              )
      
          [1478f02ce1faa31e122a61a88af498e4] => Array
              (
                  [0] => v2
                  [1] => v3
              )
      
          [0fba8cccc8dceda9fe84c3c93c20d057] => Array
              (
                  [0] => v1
                  [1] => v4
              )
      
          [c995c93b92f8fe8003ea77617760a0c9] => Array
              (
                  [0] => v1
                  [1] => v2
                  [2] => v3
                  [3] => v4
              )
      
          [8eae017bc12f0990ab42196af0a1f6a8] => Array
              (
                  [0] => v2
                  [1] => v4
              )
      
          [57c0cc445b506ba6d51dc3c2f06fd926] => Array
              (
                  [0] => v2
                  [1] => v3
                  [2] => v4
              )
      
          [18cef1bbe850dca5d2d7b6bfea795a23] => Array
              (
                  [0] => v3
                  [1] => v4
              )
      
          [e0bd0c51bfa4df20e4ad922f57f6fe0d] => Array
              (
                  [0] => v1
                  [1] => v5
              )
      
          [6a8b7681b160e28dd86f3f8316bfa16e] => Array
              (
                  [0] => v1
                  [1] => v2
                  [2] => v3
                  [3] => v4
                  [4] => v5
              )
      
          [85e95d3e4dc97e066ec89752946ccf0c] => Array
              (
                  [0] => v2
                  [1] => v5
              )
      
          [633c7cf8df43df75a24c104d9de09ece] => Array
              (
                  [0] => v2
                  [1] => v3
                  [2] => v4
                  [3] => v5
              )
      
          [769f8ebc0695f46b5cc3cd444be2938a] => Array
              (
                  [0] => v3
                  [1] => v5
              )
      
          [87028339e63fd6c2687dc5488ba0818c] => Array
              (
                  [0] => v3
                  [1] => v4
                  [2] => v5
              )
      
          [c2b28cdcef48362ceb0d8fb36a142254] => Array
              (
                  [0] => v4
                  [1] => v5
              )
      
      )
      
      1 trees found in the Forest:  Array (
          [0] => Array
              (
                  [v1] => Array
                      (
                          [child] => Array
                              (
                                  [v1] => BACK_EDGE
                                  [v2] => TREE_EDGE
                                  [v3] => FORWARD_EDGE
                                  [v4] => FORWARD_EDGE
                                  [v5] => FORWARD_EDGE
                              )
      
                          [color] => BLACK
                          [discover_time] => 0
                          [finish_time] => 9
                          [father] => 
                          [back_edge] => 1
                      )
      
                  [v2] => Array
                      (
                          [child] => Array
                              (
                                  [v1] => BACK_EDGE
                                  [v2] => BACK_EDGE
                                  [v3] => TREE_EDGE
                                  [v4] => FORWARD_EDGE
                                  [v5] => FORWARD_EDGE
                              )
      
                          [color] => BLACK
                          [discover_time] => 1
                          [finish_time] => 8
                          [father] => v1
                          [back_edge] => 1
                      )
      
                  [v3] => Array
                      (
                          [child] => Array
                              (
                                  [v1] => BACK_EDGE
                                  [v2] => BACK_EDGE
                                  [v3] => BACK_EDGE
                                  [v4] => TREE_EDGE
                                  [v5] => FORWARD_EDGE
                              )
      
                          [color] => BLACK
                          [discover_time] => 2
                          [finish_time] => 7
                          [father] => v2
                          [back_edge] => 1
                      )
      
                  [v4] => Array
                      (
                          [child] => Array
                              (
                                  [v1] => BACK_EDGE
                                  [v2] => BACK_EDGE
                                  [v3] => BACK_EDGE
                                  [v4] => BACK_EDGE
                                  [v5] => TREE_EDGE
                              )
      
                          [color] => BLACK
                          [discover_time] => 3
                          [finish_time] => 6
                          [father] => v3
                          [back_edge] => 1
                      )
      
                  [v5] => Array
                      (
                          [child] => Array
                              (
                                  [v1] => BACK_EDGE
                                  [v2] => BACK_EDGE
                                  [v3] => BACK_EDGE
                                  [v4] => BACK_EDGE
                                  [v5] => BACK_EDGE
                              )
      
                          [color] => BLACK
                          [discover_time] => 4
                          [finish_time] => 5
                          [father] => v4
                          [back_edge] => 1
                      )
      
              )
      
      )
      

      编辑 1: back_edge_exploit 方法可以被这个版本取代:

      ![private function back_edge_exploit ($back_edge_sender, $back_edge_receiver, $tree)
      {
          /*** The input of this function is a back edge, a back edge is defined as follows
          -a sender node: which stands lower in the tree and a reciever node which of course stands higher
          ***/
      
          /*** We want to get rid of loops, so check for a loop ***/
          if($back_edge_sender == $back_edge_receiver)
              return $this->L\[\] = $back_edge_sender;//we need to return cause no there is no cycle in a loop
      
          $cycle = Array();//There is always a cycle which is a combination of tree edges and on backedge 
          $edges_count = 0; //If the cycle has more than 2 nodes we need to check for forward edge
          $backward_runner = $back_edge_sender;//this walks backward up to the start of cycle
      
          attach_node://loops must be handled before this, otherwise goto will loop continously
          $edges_count++;
          $cycle\[\] =    $backward_runner; //enter the backedge sender
          $backward_runner = $tree\[$backward_runner\]\['father'\]; //backedge sender becomes his father
          if($backward_runner !== $back_edge_receiver) //if backedge sender has not become backedge reciever yet
              goto attach_node;//the loop again
          else
              $cycle\[\] = $backward_runner;
      
          if($edges_count>1 and $this->G\[$back_edge_receiver\]\['child'\]\[$back_edge_sender\] === 'FORWARD_EDGE' )
              $this->C\[\] = Array($back_edge_receiver,$back_edge_sender);
      
          $this->C\[\] = array_reverse($cycle); //store the tree edge->back_edge cycle 
      }][2]
      

      编辑 2: 我不得不说我发现back_edge_exploit 是不够的,它只会找到由树边缘和一个后边缘组成的循环。

      **** 编辑 3:**** 虽然这个解决方案被发现是不完整的,但它有一些有用的信息,甚至它本身的不足也是一条信息,所以我认为保留它可能是有用的。但我编辑答案的主要原因是我找到了另一个解决方案,我将在下面介绍。

      但在此之前,可以就 dfs 方法发出其他通知,通过遍历所有交叉、向前、树、后边缘的任何有效组合可能会发生循环。因此,找到依赖 dfs 信息的实际周期并非易事(需要额外的代码),请考虑 这个例子:

      只要涉及到新的解决方案,它在 1970 年的一篇旧论文中被描述为 James C. Tiernan (Check this link) 作为在有向图中查找所有基本循环的有效算法。还有一个没有 goto See it here

      的现代实现

      我的基本循环算法(就是名字)的实现是在 php 中,并且尽可能接近原始。我已经检查过了,它可以工作。它是关于有向图的,如果你声明你的图表有一个有向循环 v1->v2->v3 和另一个有向循环 v2->v3->v1 那么两个循环都会被发现,这是合乎逻辑的,因为它适用于有向图表。

      对于无向图,作者提出了比修改算法更好的解决方案的其他算法,但是可以通过修改图定义并为被视为无向边的长度为 2 的循环添加额外检查来完成。特别是三个节点的无向​​循环将在定义中定义两次,每个方向一次,如下所示:v1->v2->v3 和 v3->v2->v1。然后算法会找到它两次,每个方向一次,然后修改EC3_Circuit_Confirmation 上的一行可以删除额外的一行。

      节点是按顺序定义的,可以更改常量first 和邻接表以使第一个节点从零开始计数。

      这是Tiernan EC算法的php代码:

      <?php 
      
          define(first,1);    //Define how to start counting, from 0 or 1 
          //nodes are considered to be sequential 
          $G[first] = Array(2); $G[] = Array(1,3); $G[] = Array(4); $G[] = Array(1); 
      
      
          $N=key(array_slice($G, -1, 1, TRUE));//last key of g
          $H=Array(Array());
          $P = Array();
          $P[first] = first;
          $k = first;
          $C = Array();//cycles
          $L = Array();//loops
      
      ########################## ALGORITHM START #############################
      
          #[Path Extension]
          EC2_Path_Extension:  
          //scan adjacency list
              foreach($G[$P[$k]] as $j => $adj_node)
                  //if there is an adjacent node bigger than P[1] and this nodes does not belong in P
                  if( ($adj_node > $P[first]) and (in_array($adj_node, $P))===false and 
                  (count($H[$P[$k]])>0 and in_array($adj_node, $H[$P[$k]]))===false)
                  {
                      $k++;
                      $P[$k] = $G[$P[$k-1]][$j];  
                      goto EC2_Path_Extension;    
                  }
      
          #[EC3 Circuit Confirmation]  
          EC3_Circuit_Confirmation: 
          if(!in_array($P[first], $G[$P[$k]]))
              goto EC4_Vertex_Closure;
          //otherwise
          if (count($P)===1)
              $L[] = current($P);
          else
              $C[] = implode($P);
      
      
          #[EC4 Vertex Closure]
          EC4_Vertex_Closure:
          if($k===first)
              goto EC5_Advance_Initial_Vertex;
      
          $H[$P[$k]] = Array(); 
          $H[$P[$k-1]][] = $P[$k];
          unset($P[$k]);
          $k--;
          goto EC2_Path_Extension;
      
      
          #[EC5 Advance Initial Vertex]
          EC5_Advance_Initial_Vertex:
          if($P[first] === $N)
              goto EC6_Terminate;
      
          $P[first]++; 
          $k=first;
          //Reset H 
          $H=Array(Array()); 
          goto EC2_Path_Extension;
      
          EC6_Terminate:
      ########################### ALGORITHM END ##################################
      
          echo "\n\n".count($L)."$count loops found: ".implode(", ",$L)."\n\n";
          echo count($C)." cycles found!\n".implode("\n",$C)."\n";
      
          /*** Original graph found in the paper ***/ 
          //$G[first] = Array(2); $G[] = Array(2,3,4);
          //$G[] = Array(5); $G[] = Array(3); $G[] = Array(1);
      
      
          ?>
      

      【讨论】:

        【解决方案3】:

        我的建议是使用 Tarjan 算法来查找强连通分量的集合,并使用 Hierholzer 算法来查找强连通分量中的所有环。

        这是带有测试用例的 Java 实现的链接: http://stones333.blogspot.com/2013/12/find-cycles-in-directed-graph-dag.html

        【讨论】:

          【解决方案4】:

          如果一个遍历算法只访问每条边一次,那么它就不能找到所有的循环。一条边可以是多个循环的一部分。

          顺便说一句,什么是后端?

          另外,也许您应该改写/格式化您的问题。很难读。

          【讨论】:

          • 后边是搜索树中从节点到其祖先之一的边。例如。无向图的边 (A,B)、(B,C) 和 (C,A),则 (A,B) 或 (C,A) 将是后边(如果 root = A)。它不是搜索树的树边缘,它是在 DFS-VISIT 期间创建的,当边缘从新节点通向已访问(灰色)但尚未完成的节点时。
            您的第一个答案:如果一条边是多个循环的一部分,则每个循环将被遍历一次 => 将检测所有后沿 => 应检测所有循环 AFAIK。
            如果我错了,请纠正我!
          • 对不起,我必须纠正自己并同意你的“白痴”。在探索示例时,我将我的“循环”限制在三角形上,并着重于检测这些三角形。这意味着“算法”始终遵循直接导致三角形的边缘,尽管情况并非如此。例如。 2 个三角形,共享一条边(更大的循环 = 矩形),矩形可能首先作为一个循环被发现,而这 2 个三角形可能会被遗漏。反之亦然。
          猜你喜欢
          • 2014-01-02
          • 1970-01-01
          • 2010-10-07
          • 1970-01-01
          • 2018-05-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多