查找图表中的所有周期,redux

时间:2010-05-15 11:24:53

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

我知道这个问题有很多答案。但是,我发现他们中没有一个能够真正实现这一点 有人认为一个循环(几乎)与强连通组件(s。Finding all cycles in a directed graph)相同,因此可以使用为该目标设计的算法。
有人认为找到 a 循环可以通过DFS完成,并检查后端(有关文件依赖性的增强图文档)。

我现在想知道是否可以通过DFS检测图中的所有周期并检查后边缘?
http://www.me.utexas.edu/~bard/IP/Handouts/cycles.pdf(在S.O.上发现)陈述了一种基于循环基础的方法。我个人而言,我觉得它不太直观,所以我正在寻找一个不同的解决方案
编辑:我最初的意见显然是错误的。 S.“Moron”的下一个回答。
初始意见: 我的观点是它确实可以这样工作,因为DFS-VISIT(旧的DFS伪代码)刚刚进入尚未访问过的每个节点。从这个意义上讲,每个顶点都表现出一个潜在的循环开始。此外,由于DFS每次访问每个边缘,因此也会覆盖通向循环起点的每条边。因此,通过使用DFS和后沿检查,确实可以检测图中的所有周期。请注意,如果存在具有不同参与者节点数的循环(例如三角形,矩形等),则必须进行额外的工作来区分每个循环的实际“形状”。

4 个答案:

答案 0 :(得分:6)

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

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〕。

答案 1 :(得分:2)

也许这可以帮助你以某种方式,我找到了this网站,其中描述了有向图的彩色dfs。因此,您可以考虑更正我在此处提供的dfs转换。

我添加的是创建森林的一部分,以及查找所有周期的其他部分。因此,请注意,将我的代码的这两部分视为正确是不安全的。

对图论有所了解的人可能能够肯定地进行测试。 dfs部分中没有注释,因为它已在参考站点中进行了描述。我建议您以多个树为例,在纸上绘制森林(需要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 aproach进行另一次通知,通过遍历任何有效组合(包括交叉,前进,树,后边缘)可能会发生循环。因此,根据dfs信息查找实际周期并非易事(需要额外的代码),请考虑 这个例子:

enter image description here

只要涉及新的解决方案,它就会在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和邻接列表,使第一个节点从0或1开始计数。

这是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);


    ?>

答案 2 :(得分:1)

我的建议是使用Tarjan算法查找一组强连通组件,并使用Hierholzer算法查找强连通组件中的所有周期。

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

答案 3 :(得分:0)

如果遍历算法仅访问每个边缘一次,则无法找到所有周期。边缘可以是多个周期的一部分。

不过,什么是后沿?

另外,也许您应该重新定义/格式化您的问题。这很难读。