在2d数组中选择相似项组的算法

时间:2010-05-07 12:07:10

标签: c# algorithm

有一个2d项目数组(在我的例子中,它们被称为Intersections)。

某个项目作为开始。任务是找到直接或间接连接到此项目以满足特定功能的所有项目。

所以基本算法是这样的:

将开头添加到结果列表中。 重复直到没有修改:添加满足该功能的数组中的每个项目,并将结果列表中的任何项目触摸到结果列表。

我目前的实现如下:

private IList<Intersection> SelectGroup (
    Intersection start,
    Func<Intersection, Intersection, bool> select)
{
    List<Intersection> result = new List<Intersection> ();

    Queue<Intersection> source = new Queue<Intersection> ();
    source.Enqueue (start);

    while (source.Any ()) {
        var s = source.Dequeue ();
        result.Add (s);

        foreach (var neighbour in Neighbours (s)) {
            if (select (start, neighbour)
                && !result.Contains (neighbour)
                && !source.Contains (neighbour)) {
                source.Enqueue (neighbour);
            }
        }
    }

    Debug.Assert (result.Distinct ().Count () == result.Count ());
    Debug.Assert (result.All (x => select (x, result.First ())));

    return result;
}

private List<Intersection> Neighbours (IIntersection intersection)
{
    int x = intersection.X;
    int y = intersection.Y;

    List<Intersection> list = new List<Intersection> ();

    if (x > 1) {
        list.Add (GetIntersection (x - 1, y));
    }
    if (y > 1) {
        list.Add (GetIntersection (x, y - 1));
    }
    if (x < Size) {
        list.Add (GetIntersection (x + 1, y));
    }
    if (y < Size) {
        list.Add (GetIntersection (x, y + 1));
    }

    return list;
}

select函数接受一个开始项,如果第二个项满足则返回true。)

这完成了它的工作并且对于通常的阵列大小(大约20 * 20)来说是合理的。但是,我对进一步改进感兴趣。有什么想法吗?

示例(X满足其他X s,.永远不会满足:

....
XX..
.XX.
X...

在这种情况下,有2组:4个项目的中央组和左下角的单个项目组。选择组(例如通过开始项[3,3])返回前者,而后者可以使用起始项和唯一返回值[1,4]来选择。

示例2:

.A..
..BB
A.AA

这次有4组。 3 A组未连接,因此它们作为单独的组返回。较大的A和B组是连接的,但A与B无关,因此它们作为单独的组返回。

3 个答案:

答案 0 :(得分:1)

第1步:为获取巨额利益而进行微不足道的改变
简单,即时的改进:您的result.Containssource.Contains成员资格都在列表类型上,因此它们将是这些列表大小的O(n),效率不高。由于您真的不关心任何特定的排序,我会将这两个排序更改为HashSet以进行恒定时间查找。
请注意,在最坏的情况下,您当前的实现将是O(n ^ 2),这在整个数组有效时发生(当您插入最后几个元素时,您将检查每个元素与其余元素的网格)。

第2步:进一步优化
更好的结构更改:保留与Intersection数组大小相同的布尔visited数组,每次查看Intersection时,将其标记为已访问。这样,您不必每次都检查resultsource中的某些内容,更好的是,您不必重新评估select谓词。否则,给出这样的东西:

XXX
X.X
XXX

你要四次评估中心点上的select,如果价格昂贵,这可能会很糟糕。这样,你的

if (select (start, neighbour)
  && !result.Contains (neighbour)
  && !source.Contains (neighbour))

条件变为:if (!visited(neighbour) && select(start, neighbour),在任何给定的交叉点上最多只评估一次select
此外,如果你这样做,你甚至不需要再进行resultcontains哈希,因为你不会对它们进行包容性检查。

答案 1 :(得分:0)

我不是很擅长C#而是阅读你的算法描述,我可以给你一些建议:

1 /你知道动态编程还是贝尔曼算法? 我们的想法是,对于每次迭代,您只能获得最新结果并继续搜索。 Dynamic Programming

例如:

第1次迭代:x1,x2

第二次迭代:x3,x4

第三次迭代,您只搜索并与x3和x4进行比较。它将为您节省大量的CPU计算和迭代次数

2 /使用HashSet更好地搜索并获取包含

的数据

答案 2 :(得分:0)

根据tzaman的回答,我使用了HashSet。由于队列不支持散列,我发现了一项改进,只需要在结果列表中查找而不是结果和源列表:

HashSet<Intersection> result = new HashSet<Intersection> ();
result.Add (start); // need to add first item in advance

Queue<Intersection> source = new Queue<Intersection> ();
source.Enqueue (start);

while (source.Any ()) {
    var s = source.Dequeue ();

    foreach (var neighbour in Neighbours (s)) {
        if (select (start, neighbour) && !result.Contains (neighbour)) {
            result.Add (neighbour); // add immediately to hashset
            source.Enqueue (neighbour);
        }
    }
}