在图中计算簇大小时如何避免无限循环?

时间:2013-09-30 20:48:40

标签: c# algorithm graph simulation

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

Undirected square lattice graph.

在内存中组织为节点列表,使得每个节点具有其邻居节点的列表。如果给定节点也处于状态1,我想从任何节点开始计算有多少节点node[i].State == 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<Boolean> nodeStatus,并将此列表作为函数Node.GetClusterSize()的引用传递。但是,我必须在每个时间步都重置此列表,从而减慢代码速度(性能很重要!)。

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

Algorithm to count cluster size

深度优先搜索在上面的树中遍历蓝色路径进行迭代,当它到达节点3时,它知道所有邻居都已被访问过,将3标记为未访问并返回s = 1。由于3是要访问的2的下一个邻居,并且3被标记为未访问(虽然它已被访问过),但它会再次检查并且算法进入{ {1}}异常或最多返回错误的群集大小。

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

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

2)以顺序方式(不递归)实施深度优先搜索。然而,我无法想象它是如何可能的。

你有任何想法来覆盖这个问题吗?有什么建议吗?

提前谢谢!

PS:可以比此示例更大,并且网络中可能有多个黑色群集 同一时间,彼此断开连接。因此, 计算黑色元素

3 个答案:

答案 0 :(得分:8)

不要改变您尝试查询的对象;那种方式就是疯狂,因为当你注意到时,你必须取消对象的突变。

以这种方式看待它。您已定义关系。如果黑色节点之间存在任何边缘,则黑色节点与另一个黑色节点相关。当给定黑色节点时,您希望计算此关系的自反和传递闭包的大小

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

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

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

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

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岁,否则简称是手提包,哟,你好。)

答案 1 :(得分:3)

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

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;
}

另一种方法是使用generation-tag而不是Visited-flag。如果生成与目标匹配,则该节点被视为已访问。使用此方法,您无需在算法完成后重置该值。

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;
}

答案 2 :(得分:1)

您可以考虑使用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();
            }
        }
}