【问题标题】:How to avoid infinite loops when counting cluster size in a graph?在计算图中的簇大小时如何避免无限循环?
【发布时间】:2013-10-06 20:47:42
【问题描述】:

假设我有下图(箭头表示连接方向),我想计算黑色节点簇的大小:

在内存中组织为节点列表,这样每个节点都有一个其邻居节点的列表。我想从任何节点开始计算有多少节点有node[i].State == 1,如果给定节点也处于状态1。因此,我实现了一个方法Node.GetClusterSize(),在其中我计算集群大小(它基于在深度优先搜索算法中):

public class Node
{
    public Int32 State { get; private set; } // 0 = white; 1 = black;

    public Boolean Visited { get; private set; }

    public List<Node> InputNeigh { get; private set; } // list of references to
                                                       // neighbors nodes

    public Int32 GetClusterSize()
    {
        this.Visited = true;
        if (this.State == 1)
        {
            Int32 s = 1, i = 0;
            while (i < this.InputNeigh.Count)
            {
                if (!this.InputNeigh[i].Visited)
                {
                    s += this.InputNeigh[i].GetClusterSize();
                }
                i++;
            }
            this.Visited = false; // this is an important step, I'll explain why
            return s;
        }
        else
        {
            return 0;
        }
    }
    public void Evolve() { /* doesn't matter for this question */ }
}

现在,我需要将节点标记未访问,因为我在主模拟的每个时间步计算每个节点的集群大小(节点的状态会随着时间而变化,因此集群可能会在下一个时间步改变大小)。

如果我有一个外部布尔列表,而不是 Node 对象中的标志,则可以轻松解决此问题,给定元素 i 对应于节点 i:List&lt;Boolean&gt; nodeStatus,并通过此列表作为对函数 Node.GetClusterSize() 的引用。但是,我将不得不在每个时间步重置此列表,从而减慢代码速度(性能很重要!)。

上述代码的失败正是在遍历其邻居后将该节点标记为未访问。使用以下树可以更好地可视化这种情况(从左到右并假设我首先调用node[0].GetClusterSize()):

深度优先搜索遍历上面树中的蓝色路径,当它到达节点3时,它知道它的所有邻居都已被访问,将3标记为未访问并返回@987654334 @。由于32 的下一个要访问的邻居,并且3 被标记为未访问(尽管它已经被访问过),所以它再次检查并且算法进入StackOverflow 异常,或者,在最好,返回错误的集群大小。

因此,我想出了两个想法,虽然我仍然不知道如何实现它们:

1) 实现广度优先搜索算法;虽然我不知道如何将这个概念应用到所呈现的情况。

2) 以顺序方式(非递归)实现深度优先搜索。然而,我无法想象这是怎么可能的。

您有什么想法可以解决这个问题吗?有什么建议吗?

提前感谢您!

PS:图表可能比这个例子,并且在网络中可能有多个黑色集群在同一时间,彼此断开。因此,不仅仅是计算黑色元素的问题

【问题讨论】:

    标签: c# algorithm graph simulation


    【解决方案1】:

    不要改变您尝试查询的对象;这种方式很疯狂,因为正如您所注意到的,您必须取消对象的变异。

    这样看。您定义了一个关系。如果一个黑色节点直接在它们之间有任何边,则它们与另一个黑色节点相关。当给定一个黑色节点时,您希望计算此关系的自反和传递闭包的大小。

    在您的示例中,关系似乎也是对称的,因此闭包将定义一个等价类,然后您的问题是“给定一个成员,找出其等价类的大小。

    所以让我们解决更普遍的问题。

    什么是关系?正如评论者指出的那样,关系正确地是一组有序对。但是将您的关系视为一个函数是很方便的,当给定一个元素时,它会为您提供与其相关的所有元素的序列。在这种情况下:给定一个黑色节点,关系函数为您提供所有相邻黑色节点的序列。

    这里我们有一个非递归方法,当给定一个项目和一个关系时,它会计算该关系的传递闭包:

    static HashSet<T> TransitiveClosure<T>(
        Func<T, IEnumerable<T>> relation,
        T item)
    {
        var closure = new HashSet<T>();
        var stack = new Stack<T>();
        stack.Push(item); 
        while(stack.Count > 0)
        {
            T current = stack.Pop();
            foreach(T newItem in relation(current))
            {
                if (!closure.Contains(newItem))
                {
                    closure.Add(newItem);
                    stack.Push(newItem);
                }
            }
        } 
        return closure;
    } 
    

    请注意,这是带有循环检测的非递归深度优先遍历。


    练习:您可以对此实现进行哪些简单的更改,以将其变成具有循环检测的非递归广度优先遍历?


    我们很容易创建自反和传递闭包:

    static HashSet<T> TransitiveAndReflexiveClosure<T>(
        Func<T, IEnumerable<T>> relation,
        T item)
    {
      var closure = TransitiveClosure(relation, item);
      closure.Add(item);
      return closure;
    }
    

    练习:你的关系是对称的,这意味着当我们从一个节点 X 开始并访问一个邻居 Y 时,当我们处理 Y 时,它会将 X 放回堆栈中,并最终进入关闭。因此没有必要采取反身闭包。

    前面的说法不正确;它需要采取反身闭包。该论点中的哪个句子包含第一个错误?


    现在你有了一个可以很容易调用的方法:

    var cluster = TransitiveAndReflexiveClosure<Node>(
        node => from n in node.InputNeigh where n.State == node.State select n,
        someNode);
    

    现在您可以简单地询问集群的大小,如果这是您想要的。

    (请更改 InputNeigh 的名称。缩写是不酷的,哟,除非你 13 岁。)

    【讨论】:

    • 我认为这是一个非常优雅和通用的解决方案,虽然这种方法的执行时间是 O(N^2),其中 N = 节点数,不是吗? (因为检查closure 是否已经包含一个元素)
    • @Girardi closureHashSetHashSet.Contains() 是 O(1) 操作(假设合理的散列函数)。
    • @Girardi:svick 是正确的;检查哈希集中的成员资格很便宜。作为练习,在给定节点数 n 和每个节点的平均边数 e 的情况下,尝试计算该算法的渐近成本。 (请注意,e 可能是 n 的函数,但在常规图中显然不能超过 n。)
    • @EricLippert 这个答案非常棒;我喜欢人们用数学来解决编程问题!但我不确定你的术语。关系不是函数的一种类型,而是反过来。在这种情况下,您可以将其描述为:如果 E 是 A 上的等价关系,则将每个元素映射到其等价类 a -> [a]_E。你所说的传递闭包更像是一个等价类(没有一个元素)。传递闭包是满足传递性的现有关系的扩展(例如,aEb 和 bEc,但是 !aEc。传递闭包添加 aEc)。
    • @roliu:数学上,关系是一组有序对;你是正确的,一个函数也是一组有序对,限制是没有两对具有相同的第一项。但是对于我们在这里讨论的那种有限关系,它相当于说关系是将第一个元素带到所有第二个元素的集合的函数。在这个特定的例子中,关系似乎是对称的和自反的,因此关系的传递闭包是等价关系。
    【解决方案2】:

    顺序广度优先搜索,使用列表重置已访问标志:

    public int GetClusterSize()
    {
        if (State != 1) return 0;
    
        List<Node> cluster = new List<Node>();
    
        Stack<Node> stack = new Stack<Node>();
        stack.Push(this);
        while (stack.Count > 0)
        {
            Node node = stack.Pop();
            if (node.Visited) continue;
    
            node.Visited = true;
            cluster.Add(node);
            foreach (var neigh in node.InputNeigh)
            {
                if (neigh.State == 1 && !neigh.Visited)
                {
                    stack.Push(neigh);
                }
            }
        }
    
        int clusterSize = cluster.Count;
        foreach (var node in cluster)
        {
            node.Visited = false;
        }
    
        return clusterSize;
    }
    

    另一种方法是使用生成标记而不是已访问标记。如果生成与目标匹配,则认为该节点已访问。使用这种方法,您无需在算法完成后重新设置值。

    private static int NextGeneration = 0;
    
    public int Generation { get; private set; }
    
    public int GetClusterSize()
    {
        return GetClusterSizeInternal(NextGeneration++);
    }
    
    private int GetClusterSizeInternal(int target)
    {
        if (State != 1) return 0;
    
        Generation = target;
    
        int sum = 0;
        foreach (var neigh in InputNeigh)
        {
            if (neigh.State == 1 && neigh.Generation != target)
            {
                sum += neigh.GetClusterSizeInternal(target);
            }
        }
    
        return sum;
    }
    

    相同,但没有递归:

    private static int NextGeneration = 0;
    
    public int Generation { get; private set; }
    
    public int GetClusterSize()
    {
        if (State != 1) return 0;
    
        int target = NextGeneration++;
    
        Stack<Node> stack = new Stack<Node>();
        stack.Push(this);
    
        int count = 0;
        while (stack.Count > 0)
        {
            Node node = stack.Pop();
            if (node.Generation == target) continue;
    
            node.Generation = target;
    
            count++;
            foreach (var neigh in node.InputNeigh)
            {
                if (neigh.State == 1 && neigh.Generation != target)
                {
                    stack.Push(neigh);
                }
            }
        }
    
        return count;
    }
    

    【讨论】:

    • 好吧,如果我想在下一个模拟时间步中计算同一节点的集群大小,我认为生成方法也需要重置,不是吗?也许我可以在方法GetClusterSize() 中初始化NextGeneration 计数器,然后开始你的实现...
    • 如果你想避免递归(根据深度可能会炸毁你的堆栈),你可以使用队列或堆栈来推动节点访问。如果你使用队列,它将是 BFS,如果你使用堆栈,它将是 DFS
    • @Girardi No. GetClusterSizeInternal 每次都需要一个唯一的编号,以便能够将访问的节点与该方法的先前执行区分开来。 NextGeneration 是静态的,所以会在所有节点之间共享。
    • 另外,我会从Node 中提取访问逻辑,并让图表上的外部访问者进行计数。
    • @ChrisWue 我对此非常重视。在 OO 方法中,我认为将计数器与节点分开是最好的方法,它还允许并行化。
    【解决方案3】:

    您可以考虑 System.Collections.BitArray 而不是 List

    你可以试试这样的(伪代码):

    Stack<T> stk
    stk.Push(node1)
    grandparentnode = null
    count = 0
    if node1 state is 1 count++
    while (node = stk.Pop())
        foreach connect in node.connections
            if the connect state is 1
                if connect != grandparentnode 
                    stk.Push(connect)
                    count++
        grandparentnode = node
    

    我认为这行得通,但是图表很难,而且我的大脑很小并且充满了错误:-(

    根据评论添加到帖子。

    祖父节点是通过保持不断滚动的祖父/父/子关系来消除“已访问”字段的错误尝试。我是一个优秀的程序员,但也许对图论没有兴趣(这就是为什么我会被这些问题所吸引:-D)无论如何,这里有一个完整的程序,其中包含我最初的想法的修改代码。这取决于节点的网格状、双连接排列以及为每个节点设置一个独特的数字递增标签的想法。这些限制对于您的使用可能过于具体。我用了一本字典,但它永远不应该包含超过 4 个项目。为这样一个小集合创建一个优化的自定义类将提高性能。这应该消除保持“已访问”状态、快速运行而不是递归的需要。要查找任何子树,您需要从该树的最低标签节点开始。

    当然,它也有可能不走运地得出正确的答案 :-) 最坏的情况是让任何有兴趣的人使用完整的程序框架。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Xml.Linq;
    
    namespace stackoverflow1 {
        class Program {
            class Node {
                public int Number;
                public int State = 0;
                public List<Node> Connects = new List<Node>();
                public Node(int num, int state) {
                    Number = num;
                    State = state;
    
                    }
                }
    
            static void Main(string[] args) {
                var nodes = new List<Node>();
                nodes.Add(new Node(0, -1)); // not used
                nodes.Add(new Node(1, 1));
                nodes.Add(new Node(2, 1));
                nodes.Add(new Node(3, 1));
                nodes.Add(new Node(4, 0));
                nodes.Add(new Node(5, 1));
                nodes.Add(new Node(6, 1));
                nodes.Add(new Node(7, 0));
                nodes.Add(new Node(8, 0));
                nodes.Add(new Node(9, 0));
                nodes[1].Connects.Add(nodes[2]);
                nodes[1].Connects.Add(nodes[4]);
    
                nodes[2].Connects.Add(nodes[1]);
                nodes[2].Connects.Add(nodes[3]); 
                nodes[2].Connects.Add(nodes[5]);
    
                nodes[3].Connects.Add(nodes[2]); 
                nodes[3].Connects.Add(nodes[6]);
    
                nodes[4].Connects.Add(nodes[1]);
                nodes[4].Connects.Add(nodes[5]); 
                nodes[4].Connects.Add(nodes[7]);
    
                nodes[5].Connects.Add(nodes[2]);
                nodes[5].Connects.Add(nodes[4]); 
                nodes[5].Connects.Add(nodes[6]);
                nodes[5].Connects.Add(nodes[8]);
    
                nodes[6].Connects.Add(nodes[3]);
                nodes[6].Connects.Add(nodes[5]); 
                nodes[6].Connects.Add(nodes[9]);
    
                nodes[7].Connects.Add(nodes[4]);
                nodes[7].Connects.Add(nodes[8]); 
    
                nodes[8].Connects.Add(nodes[5]);
                nodes[8].Connects.Add(nodes[7]); 
                nodes[8].Connects.Add(nodes[9]);
    
                nodes[9].Connects.Add(nodes[6]);
                nodes[9].Connects.Add(nodes[8]); 
    
                var dict = new Dictionary<int, Node>();
                foreach (var n in nodes) {
                    if (n.State == 1) {
                        dict.Add(n.Number, n);
                        break;
                        }
                    }
    
                int count = dict.Count;
                while (dict.Count > 0) {
                    foreach (var k in dict.Keys.ToArray()) { // retains node order
                        var n = dict[k]; // get the first node in number order
                        dict.Remove(k);
                        foreach (var node in n.Connects) { // look over it's connections/children
                            if ((node.State == 1) 
                            &&  (node.Number > n.Number))  {
                                if (dict.ContainsKey(node.Number) == false) {
                                    // only add if this is has a greater number than the one
                                    // being considered because lower values have already been
                                    // processed
                                    dict.Add(node.Number, node);
                                    count++;
                                    }
                                }
                            }
                        }
                    }
    
                Console.WriteLine("Count = {0}", count);
                Console.ReadKey();
                }
            }
    }
    

    【讨论】:

    • 好吧,只看你的代码,我注意到一个可能的问题:我不太明白grandparentnode在这里的作用。你能澄清一下这个想法吗? PS:感谢BitArray 引起我的注意。对于这个特定的问题,它可能就足够了,但这是对我的实际问题的抽象,其中状态变量实际上是Double,因为它应该是连续的而不是离散的。
    猜你喜欢
    • 2019-05-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-04
    • 2018-02-26
    相关资源
    最近更新 更多